Moving a site out of Framer is not mainly a job of recreating rectangles in CSS. The difficult part is preserving the invisible system around the rectangles: URLs, content, metadata, images, language relationships, redirects, analytics, and all the small assumptions accumulated while the site was live.
I migrated my portfolio into a statically exported React project, keep the blog as files, use AI to accelerate implementation and review, and deploy the finished output to Vercel. This guide explains the workflow I would use again.
If you want the reasoning and real cost comparison first, read Why I Left Framer for an AI-Assisted Static Site on Vercel.
The target architecture
content/articles/ versioned blog content
public/images/ static image assets
public/audio/ pre-generated narration
app/ routes and page components
lib/content.ts content loader
scripts/ migration and metadata tools
tests/ rendered-output checks
dist/client/ deployable static site
The browser receives static files. Git stores the source of truth. Vercel builds the repository and publishes dist/client. Dynamic behavior can still run in the browser, but a server is not required to render an article.
Step 1: inventory the live site before touching code
Create a list of every URL that matters. Include:
primary pages and localized versions;
every CMS article slug;
canonical URLs and metadata;
images, downloads, audio, and externally hosted assets;
forms and third-party embeds;
analytics, verification tags, and consent behavior;
redirects already in use;
pages receiving impressions or backlinks.
Export the Framer CMS collection to CSV and keep the original export untouched in a backup folder. Take full-page screenshots of important routes at desktop and mobile sizes. Screenshots are not source code, but they are excellent regression evidence.
Do this before redesigning anything. A migration and a redesign can happen together, but you still need to know whether a difference is intentional.
Step 2: choose a static-capable stack
You do not need my exact dependencies. Astro, Eleventy, Next.js static export, or another static-capable framework can all work. The important requirements are:
one deterministic output file per public route;
support for metadata and localized routes;
a build command that fails loudly;
content that does not depend on a proprietary editor;
a deployable output directory.
My implementation uses React, Vite, and vinext:
npm install
npm run dev
npm run build
npm test
The production export is enabled in next.config.ts:
const nextConfig = {
output: "export",
};
export default nextConfig;
Because vinext builds through Vite, the finished client output lands in dist/client. That directory, not the source repository, is what the host serves.
Step 3: convert CMS records into portable article files
Each article in this site lives at:
content/articles/<slug>/en.md
content/articles/<slug>/es.md
The file begins with strict front matter and continues with the article body:
---
slug: "example-article"
locale: "en"
title: "Example Article"
description: "A useful summary."
date: "2026-08-28T00:00:00.000Z"
draft: false
type: "Blog"
cluster: "framer-cms-technical-seo"
tags: ["Migration", "Static site"]
audio: ""
image: "/images/blog/default-cover.png"
canonical: ""
featured: false
---
<p>The article begins here.</p>
I wrote a migration script for the CSV archive rather than pasting dozens of articles by hand. The script normalizes slugs, converts metadata into the expected shape, preserves drafts, strips unsafe scripts and inline event handlers, and assigns a temporary local cover when an image has not yet been migrated.
The rule worth keeping is simple: never make the importer destructive. Keep the original CSV, write generated content into a separate structure, and make repeated runs deterministic.
Step 4: build one content loader
The content loader reads the front matter, returns the article body, filters drafts, sorts by date, and supports a locale parameter. Every consumer should use that loader: the blog index, article routes, related-post navigation, RSS, and sitemap.
Centralizing this logic prevents one of the most common static-site mistakes: a draft disappearing from the index while remaining discoverable through the sitemap or a generated route.
getAllArticles({ locale: "en" })
.filter(article => !article.draft)
.sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
The individual article route performs the same draft check and returns not found when the content is not public.
Step 5: preserve URL structure before improving it
If the Framer article lived at /blog/my-article, keep that URL unless there is a strong reason to change it. A prettier route is rarely worth losing established links, shares, and search history.
When a route must change, add a permanent redirect in the same release:
{
"source": "/writing/:slug",
"destination": "/blog/:slug",
"permanent": true
}
Do the same when consolidating overlapping posts. Move the useful content into the stronger article, make the weaker article a draft, and redirect its old URL to the surviving canonical page. Do not ask Google to remove a URL that should transfer its signals through a redirect.
Step 6: rebuild metadata as output, not decoration
A successful visual migration can still be an SEO regression. The build should produce and verify:
unique titles and descriptions;
self-referencing canonical URLs;
language attributes and reciprocal alternates where translations exist;
Open Graph metadata;
structured data that matches visible content;
robots.txt;sitemap.xml;rss.xml.
In my project, a post-build script writes the deployable metadata files after the static routes have been exported. Draft articles are excluded from all three feeds.
Step 7: treat localization as separate content
A translated route should exist only when a real translation exists. Do not publish English text under an /es/ URL and hope a language tag makes it Spanish.
This project uses an explicit file rule:
en.md exists → publish /blog/slug
es.md exists → publish /es/blog/slug
es.md missing → do not claim a Spanish translation
The sitemap adds alternates only for pairs that genuinely exist. A rendered-output test counts translated pages and confirms their HTML language is Spanish.
Step 8: move assets out of platform URLs carefully
Download images at their best available quality and keep the originals in a root-level backup folder. Create optimized public copies separately. Never solve a performance problem by deleting the only high-resolution source.
Use stable local paths:
backup/original-images/article-hero.png
public/images/blog/article-hero.webp
Update alt text while migrating, but do not invent descriptions unrelated to the image. Scan the exported content for old Framer CDN URLs after the migration; remote assets are easy to miss because they continue working until they do not.
Step 9: add AI without handing it the steering wheel
AI is most useful here as a fast collaborator across several narrow tasks:
map repeated Framer patterns into reusable components;
write migration scripts and validation checks;
identify duplicated CSS and inconsistent tokens;
compare screenshots against a reference;
draft metadata and image alt text for review;
find internal links pointing to retired routes;
create tests around bugs that already happened.
The workflow is not “ask AI to rebuild the website.” It is:
define one bounded outcome;
let the assistant inspect the actual repository;
review the proposed files and assumptions;
run the build and tests;
open the result at real breakpoints;
commit a change small enough to understand.
AI makes the loop faster. Git makes the loop recoverable. Tests make the loop trustworthy.
Step 10: configure Vercel as a static host
Connect the Git repository to Vercel and describe the build explicitly:
{
"framework": null,
"buildCommand": "npm run build",
"outputDirectory": "dist/client",
"cleanUrls": true
}
Every push can now create a deployment. Preview deployments make visual review safer, while production remains a promotion of a specific Git state.
Vercel Hobby currently costs $0, but it is explicitly intended for personal, non-commercial use. Review the official plan terms before treating it as the permanent hosting plan for a commercial site. Pro is the appropriate reference when the project crosses that boundary.
Step 11: test the exported site, not only development mode
A local development server can hide static-export problems. My test command performs a production build and then inspects the files that will actually be deployed.
The minimum useful checks are:
canonical pages exist as static HTML;
robots, sitemap, and RSS exist in the output directory;
drafts are absent;
only genuine translations appear under localized routes;
legacy redirects remain configured;
internal links do not point to retired articles.
npm test
A green test suite is not a design review. I still inspect the homepage, blog index, one long article, navigation, footer, mobile layout, keyboard behavior, and reduced-motion behavior in a browser.
Step 12: switch the domain without improvising
Lower DNS TTL in advance if the provider allows it.
Add the domain to Vercel and note the required DNS records.
Deploy and test through the Vercel preview domain first.
Confirm canonical URLs still reference the real production domain.
Update DNS through the registrar.
Verify HTTPS, the
wwwredirect, sitemap, robots file, and key article URLs.Resubmit the sitemap in Google Search Console and monitor indexing.
Do not cancel the old platform before the new domain, redirects, and critical pages have been verified. A few days of overlap is cheaper than an avoidable outage.
The migration checklist
Original CMS export backed up.
High-quality images preserved.
Every public Framer URL mapped to a destination.
Articles imported with stable slugs.
Drafts excluded everywhere.
Real translations separated from fallbacks.
Canonicals, schema, sitemap, robots, and RSS generated.
Redirects configured before launch.
Production build tested.
Desktop and mobile screenshots reviewed.
Domain and HTTPS verified.
Search Console sitemap resubmitted.
When this model is the wrong choice
Do not migrate purely to save money if nobody wants to maintain code. A static repository is a poor replacement for a visual CMS when a non-technical team publishes daily, experiments require an integrated marketing platform, or layout changes must happen without engineering review.
It works especially well for portfolios, documentation, editorial sites, and relatively stable content where ownership, performance, and custom behavior matter more than drag-and-drop publishing.
The result is not “a free website made by AI.” It is a small software project with low infrastructure costs, versioned content, explicit trade-offs, and an AI-assisted maintenance workflow.
That description is less magical. It is also the reason I trust it.
