Integrating Webflow and Next.js
⚠️ Update: We launched DesignSync.js! A Next.js template that imports Webflow sites. It also optimizes images and supports Webflow interactions and animations. So check it out if you want to instantly get the shortcut to building beautiful Next.js sites!
Overview
Webflow is a fantastic no-code website builder for designers. It's perfect for small use cases, but sometimes has some quirks when trying to scale:
- Images aren't optimized for viewport or device
- Developers can't control any server side logic
- Developers can't manipulate Webflow's live code
- Difficult to build web apps
- ... and probably lots of other stuff I haven't encountered yet
In this article we'll explore how to do an Automated Design Integration (ADI) with Webflow and Next.js. If you've read my post on ADI patterns, the pattern we're trying to implement here is the "Embed Pattern". Which means we're embedding the design onto another platform (aka the core platform), so the design can take advantage of some of the core platform's features that maybe the design platform doesn't have.
Benefits
By coupling Webflow with Next.js, we basically get the best of both worlds. Designers have full control over the design in Webflow. Developers have full control over the code in Next.js. A developer can still create any pages they might need in Next.js for app-related functionality, or embed components onto the page code coming from the Webflow site.
This also works great for hybrid sites that have content in multiple sources. The designers can still design in Webflow, and the developers can do things like fetch WordPress, Shopify, or API data in Next.js and splice the content into the Webflow template.
Both services have are free for small use cases. So if your needs are small, you can have a very powerful site at no extra cost.
Lastly, we can get better performance & SEO with a few tweaks. We'll cover that in a bit.
Drawbacks
The only real drawback I've noticed so far is that Webflow's JS doesn't work on subsequent navigation. So interactions and animations probably won't work all the time.
This is fixable by either adding your own JS that works in React, or by just avoiding interactions altogether and using a library to handle that kind of thing.
Pulling Webflow Content Into Next.js
This tutorial will assume you're already somewhat familiar with Node.js, Next.js, and Webflow. I'm using Yarn as a package manager, but npm would work just fine too.
First create a Next.js app on your computer if you haven't already. Create a Webflow site as well if you don't already have one.
First let's try to just get the Next.js project's homepage looking like the Webflow homepage. To do this we're going to need to fetch Webflow content. We can do this with Axios. We'll also need a library called Cheerio to parse HTML with Node.
So let's install those:
yarn add axios
yarn add cheerio
First let's just try to render body content. Open up your pages/index.js
file and replace the contents with this:
// ./pages/index.js
export default function Home(props) {
return <div dangerouslySetInnerHTML={{ __html: props.bodyContent }} />
}
export async function getStaticProps(ctx) {
// Import modules in here that aren't needed in the component
const cheerio = await import(`cheerio`)
const axios = (await import(`axios`)).default
// Fetch HTML
let res = await axios(process.env.WEBFLOW_URL).catch((err) => {
console.error(err)
})
const html = res.data
// Parse HTML with Cheerio
const $ = cheerio.load(html)
const bodyContent = $(`body`).html()
// Send HTML to component via props
return {
props: {
bodyContent,
},
}
}
The Webflow URL is referenced as an environment variable. To add that to your local environment, create a file in the root of your project called .env.local
and add the following contents:
# Replace the URL with your own Webflow site; no trailing slash
WEBFLOW_URL = "https://business-starter-template.webflow.io"
Now if you run yarn dev
, you'll see the content displays, but a lot of styles aren't there. That's because we didn't load the <head>
content, we only got the <body>
.
Parsing Webflow Content
We were able to just dump the body content in there with React's dangerouslySetInnerHTML. But this won't work for the <head>
content. We'll need to actually convert that string of HTML into something that JSX can understand. For this we can use the html-react-parser module.
yarn add html-react-parser
Right under where we grabbed the <body>
content with Cheerio, we'll get the <head>
stuff too and also send that along as props.
// ./pages/index.js
...
// Parse HTML with Cheerio
const $ = cheerio.load(html)
const bodyContent = $(`body`).html()
const headContent = $(`head`).html()
// Send HTML to component via props
return {
props: {
bodyContent,
headContent
},
}
Then in our component, we can parse and convert the <head>
contents to JSX and add it to the document with next/head.
// ./pages/index.js
import Head from 'next/head'
import parseHtml from 'html-react-parser'
export default function Home(props) {
return (
<>
<Head>
{parseHtml(props.headContent)}
</Head>
<div dangerouslySetInnerHTML={{__html: props.bodyContent}} />
</>
)
}
...
You should now see the styled homepage of your Webflow site on your Next.js app. You might want to delete the default pages/_app.js
file, as it's probably pulling in a global style sheet that is/will conflict with the Webflow one.
Routing
At this point, Next.js isn't doing anything with the pages that Webflow can't already do. We're pretty much just showing a single Webflow page as-is. One benefit of Next.js is client-side routing, which can give your website app-like speed when it comes to internal navigation. Let's add that to our Next.js / Webflow project.
Since we already have the logic we want on the index file, we can just reuse it. But we'll need a getStaticPaths function that tells Next.js all the paths that are possible. We can get these paths by reading them from the Webflow site's sitemap using the sitemap-links module. Let's install it:
yarn add sitemap-links
Now we need to add a dynamic path that catches all other routes. This way we can render all Webflow pages, not just the homepage. We can do this by adding a a file in the pages
directory called [...path].js
.
// ./pages/[...path].js
import GetSitemapLinks from 'sitemap-links'
import DynamicPath, { getStaticProps } from './index'
export default DynamicPath
export { getStaticProps }
export async function getStaticPaths() {
// Fetch links from Webflow sitemap
const sitemapLink = process.env.WEBFLOW_URL + `/sitemap.xml`
const links = await GetSitemapLinks(sitemapLink).catch((err) => {
console.error(err)
})
// Extract paths from absolute links
const paths = []
for (let link of links) {
let url = new URL(link)
const path = url.pathname.replace(`/`, ``).split(`/`)
if (!path.length || !path[0]) continue
paths.push({
params: { path },
})
}
return {
paths: paths,
fallback: `blocking`,
}
}
Now we'll add some extra logic to the component in index.js
to parse and transform regular HTML links into Next.js links.
// ./pages/index.js
import Head from 'next/head'
import Link from 'next/link'
import parseHtml, { domToReact } from 'html-react-parser'
import get from 'lodash/get'
// Determines if URL is internal or external
function isUrlInternal(link){
if(
!link ||
link.indexOf(`https:`) === 0 ||
link.indexOf(`#`) === 0 ||
link.indexOf(`http`) === 0 ||
link.indexOf(`://`) === 0
){
return false
}
return true
}
// Replaces DOM nodes with React components
function replace(node){
const attribs = node.attribs || {}
// Replace links with Next links
if(node.name === `a` && isUrlInternal(attribs.href)){
const { href, ...props } = attribs
if(props.class){
props.className = props.class
delete props.class
}
return (
<Link href={href}>
<a {...props}>
{!!node.children && !!node.children.length &&
domToReact(node.children, parseOptions)
}
</a>
</Link>
)
}
}
const parseOptions = { replace }
export default function Home(props) {
return (
<>
<Head>
{parseHtml(props.headContent)}
</Head>
{parseHtml(props.bodyContent, parseOptions)}
</>
)
}
...
This might look like a lot, but it's basically just adding some parsing options to find and replace any <a>
links with the Next.js <Link>
.
We'll also need to change the getStaticProps
function to determine which Webflow page to fetch based on the URL path, rather than just fetching the homepage.
// ./pages/index.js
...
export async function getStaticProps(ctx) {
// Import modules in here that aren't needed in the component
const cheerio = await import(`cheerio`)
const axios = (await import(`axios`)).default
// Use path to determine Webflow path
let url = get(ctx, `params.path`, [])
url = url.join(`/`)
if(url.charAt(0) !== `/`){
url = `/${url}`
}
const fetchUrl = process.env.WEBFLOW_URL + url
// Fetch HTML
let res = await axios(fetchUrl)
.catch(err => {
console.error(err)
})
const html = res.data
// Parse HTML with Cheerio
const $ = cheerio.load(html)
const bodyContent = $(`body`).html()
const headContent = $(`head`).html()
// Send HTML to component via props
return {
props: {
bodyContent,
headContent
},
}
}
Now that your Next.js app can parse and transform all your Webflow links to Next.js links, it should be transitioning and rendering pages much quicker than a vanilla Webflow site.
Fixing Fonts
As mentioned in the drawbacks section, some JS doesn't work. This is a problem if your template is using Google Fonts. If you are, they likely aren't loading in properly because Google Fonts uses a little JavaScript snippet to initiate.
To do this, you can just delay the webfont a little bit by transforming it in the pages/index.js
file:
// ./pages/index.js
...
// Replaces DOM nodes with React components
function replace(node){
const attribs = node.attribs || {}
// Replace links with Next links
if(node.name === `a` && isUrlInternal(attribs.href)){
const { href, ...props } = attribs
if(props.class){
props.className = props.class
delete props.class
}
return (
<Link href={href}>
<a {...props}>
{!!node.children && !!node.children.length &&
domToReact(node.children, parseOptions)
}
</a>
</Link>
)
}
// Make Google Fonts scripts work
if(node.name === `script`){
let content = get(node, `children.0.data`, ``)
if(content && content.trim().indexOf(`WebFont.load(`) === 0){
content = `setTimeout(function(){${content}}, 1)`
return (
<script {...attribs} dangerouslySetInnerHTML={{__html: content}}></script>
)
}
}
}
...
I'm not entirely sure why this works, but it does. If someone has a more elegant solution, let me know in the comments.
Automated Integration
This wouldn't be true ADI unless the integration was automated. What we really want is for the Next.js site to automatically update every time the Webflow design changes.
Netlify and Vercel are both great hosts for Next.js sites. For this tutorial we'll be using Vercel to host our Next.js project. If you haven't already, deploy your Next.js project to Vercel. Remember to set the WEBFLOW_URL
to your Webflow site URL without the trailing slash.
In your Vercel dashboard, you can create a deploy hook in the "git" section. Copy this URL.
Then, in your Webflow site's dashboard, create a Webhook URL in the "Integrations" tab. Select "Site publish" as the trigger type, and paste in the URL you got from Vercel as the "Webhook URL".
Now whenever your Webflow site publishes any changes, it will send a notification to Vercel to rebuild the Next.js project.
Going Further
Here's a link to the final source code for this project. This is just a bare-bones implementation of this integration. Just a few ideas on how we could take this further:
- Optimizing Webflow images with the Next.js image module
- Setting up a Next.js sitemap using Webflow page routes
- Getting redirects created in Webflow's dashboard to work in Next.js
- Getting Webflow's interactions and animations to work in Next.js
- Persistent header/footer between page navigation
- Transforming code for better Google Lighthouse & Core Web Vitals metrics
Let me know in the comments if you want me to cover one of these additional enhancements or anything else and I'll post a followup. I'd also love to know what you're working on and how this might help!