Framework7, Next.js & Server-Side Rendering? Yes!

In Framework7 v6 release announcement we mentioned the full server-side rendering support. Now it is time for practice.

TL;DR

You can fork this **framework7-nextjs-starter** project right away and start hacking on it.

Create Next.js Project

First, we need to create a new Next.js project. We will use create-next-app CLI tool. Run in terminal:

npx create-next-app f7-next-app

This command will create a project in a /f7-next-app directory.

Install Framework7

Let's move to our project directory (/f7-next-app) directory and install required Framework7 dependencies. We need to install Framework7 itself, Framework7-React (as we develop a React app), Framework7 and Material icons:

npm i framework7 framework7-react framework7-icons material-icons

Setup Environment Variables

In project root folder we need to create two env files:

  • .env.development - with environment variables for development
  • .env.production - with environment variables for production build

In this files we need to define NEXT_PUBLIC_HOST variable, that will point to our webapp host. During development it will be [https://localhost:3000](https://localhost:3000,) (default Next.js dev server), and for production you need to specify the host where the app will be deployed:

# .env.development
NEXT_PUBLIC_HOST=https://localhost:3000
# .env.production
NEXT_PUBLIC_HOST=https://my-framework7-website.com

Routing

Before, we start hacking into our app files, let's figure out how to handle routing here.

Next.js has an amazing built-in router, but unfortunately it doesn't fit for complex page transitions used in Framework7. To solve this issues we need to use both Next.js (to get initial page component on server-side) and Framework7 (on client-side) routers.

To support server-side routing and have a correct response with initially requested page we must keep our pages in structure required by Next.js (e.g. in ./pages folder). And we need to match our Framework7 router to this pages.

For example, if we have the following pages in our Next.js app:

  • ./pages/index.js
  • ./pages/about.js
  • ./pages/blog/[postID].js - dynamic route

We need to match it to Framework7 React router and pass this routes to our Framework7 App. So Framework7 routes for such structure will be the following:

const routes = [
  {
    path: '/',
    asyncComponent: () => import('./index'),
  },
  {
    path: '/about',
    asyncComponent: () => import('./about'),
  },
  {
    path: '/blog/:postID',
    asyncComponent: () => import('./blog/[postID]'),
  },
];

Pay attention that we use asyncComponent because we don't want (and more likely don't need) to add all routes/pages content into initial page JavaScript file.

_app.js

Now, let's move to our main app component. We need to setup and tweak our main App layout in pages/_app.js file. By default (created by create-next-app) it should look like this:

import '../styles/globals.css';

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

export default MyApp;

Let's modify it to the following:

// Import Framework7
import Framework7 from 'framework7/lite-bundle';
// Import Framework7-React and components
import Framework7React, { App, View } from 'framework7-react';
// Next router
import { useRouter } from 'next/router';

// Import icons and styles
import 'framework7/framework7-bundle.css';
import 'framework7-icons/css/framework7-icons.css';
import 'material-icons/iconfont/material-icons.css';
import '../styles/globals.css';

// Install Framework7 React plugin for Framework7
Framework7.use(Framework7React);

// App routes
const routes = [
  {
    path: '/',
    asyncComponent: () => import('./index'),
  },
];

function MyApp({ Component, pageProps }) {
  // current Next.js route
  const router = useRouter();
  /*
    Here we need to know (mostly on server-side) on what URL user opens our app
  */
  const url = `${process.env.NEXT_PUBLIC_HOST}${router.asPath}`;

  return (
    /*
      Here we pass initial server URL and routes to the Framework7's App.
      It is required because Framework7 will be initialized on server-side,
      and we need to know this URL to correctly load pages by Framework7 router
    */
    <App url={url} routes={routes}>
      {/*
        Create main View.
        Apparently we need to enable browserHistory to navigating by URL
      */}
      <View
        main
        browserHistory
        browserHistorySeparator=""
        browserHistoryInitialMatch={true}
        browserHistoryStoreHistory={false}
        url="/"
      >
        {/*
          Initial page components (returned by Next.js).
          Here it is mandatory to set `initialPage` prop on it.
        */}
        <Component initialPage {...pageProps} />
      </View>
    </App>
  );
}

index.js

We are almost ready. What is left is to change our main index page (located in index.js ) to be a Framework7-React page:

import { Page, Navbar, Block } from 'framework7-react';

export default function Home() {
  return (
    <Page>
      <Navbar title="Framework7 Next.js" />
      <Block>Hello world from Next.js</Block>
    </Page>
  );
}

Launch 🚀

Now, let's launch our dev server

npm run dev

And we should see something similar to this on https://localhost:3000

But was it rendered on server-side? Let's check response source code:

Great, as we see server-side rendering works.

Navigation

As mentioned above for navigating and router we use ONLY Framework7's router, its methods and components.

Let's create dynamic route page in ./pages/blog/[postID].js file with the following content:

import { Block, Navbar, Page } from 'framework7-react';

export default function BlogPost(props) {
  /* 
   we use Framework7 router and its features
  */
  const { postID, f7route } = props;
  return (
    <Page>
      <Navbar title="Post" backLink="Back" />
      <Block strong>
        <p>
          This is a dynamic route loaded from <code>/blog/[postID].js</code>{' '}
          page component.
        </p>
        <p>
          Route URL: <b>{f7route.url}</b>
        </p>
        <p>
          Post ID: <b>{postID}</b>
        </p>
      </Block>
    </Page>
  );
}

And now we need to add this route to our Framework7 routes in _app.js :

// ...

// move our routes array out of component
const routes = [
  {
    path: '/',
    asyncComponent: () => import('./index'),
  },
  // add dynamic Framework7 route to ./blog/[postID].js page
  {
    path: '/blog/:postID',
    asyncComponent: () => import('./blog/[postID]'),
  },
];

function MyApp({ Component, pageProps }) {
  // ...
  return (
    <App url={url} routes={routes}>
      {/* ... */}
    </App>
  );
}

export default MyApp;

And link to the page, somewhere on our home page:

<a href="/blog/45">Blog post</a>

Everything works as intended:

Bonus: Server-Side Theme Detection

As you may know Framework7 supports iOS, Material Design and Aurora themes. Theme can be specified on main App component like so:

<App theme="ios">

Or it can be set to auto and Framework7 will decide which theme to use based on user device. And if we use server-side rendering, it is not recommended to use auto theme senselessly.

For example, we set theme to auto, and user visits our web app with iOS device. Framework7 will be initialized on server-side, and apparently it will not detect server environment as iOS device and will enable material theme. Then on client side, it will detect it as iOS device and set theme to ios , but HTML layout rendered on server-side can be different (the one that required for material theme), which can produce as server-client layouts will not match.

Simple solution will be to just use single theme. But that is not for us. With Next.js getInitialProps we can actually correctly detect user's device on server-side based on his user-agent !

To do so, let's add getInitialProps function to our _app.js and see how get and use user-agent:

function MyApp({ Component, pageProps, userAgent }) {
  // pass userAgent to Framework7's App component
  return (
    <App url={url} routes={routes} userAgent={userAgent}>
      {/* ... */}
    </App>
  );
}

MyApp.getInitialProps = async ({ ctx }) => {
  // get user-agent string on server-side
  if (ctx && ctx.req && ctx.req.headers) {
    // pass it to our component props
    return {
      userAgent: ctx.req.headers['user-agent'],
    };
  }
  return {};
};

export default MyApp;

Now, theme detection will work correctly and we can safely use condition rendering even on server-side, e.g.:

import { Page, Navbar, Block, theme } from 'framework7-react';

export default function Home() {
  return (
    <Page>
      <Navbar title="Framework7 Next.js" />
      <Block>
        {theme.ios && <p>Hello to iOS user!</p>}
        {theme.md && <p>Hello to Android user!</p>}
      </Block>
    </Page>
  );
}

P.S.

If you love Framework7, please, support project by donating or pledging:

Your support means a lot for us!