Codementor Events

Build a Restaurant Website with Next.js and Cosmic

Published Apr 27, 2022Last updated Jul 21, 2022
Build a Restaurant Website with Next.js and Cosmic

In this tutorial, I will show you how to build a fully mobile responsive restaurant website using Next.js and Cosmic Headless CMS. The Figma template can be found on ui8.net.

Bonus features include deploying to Vercel and setting up automatic static content revalidation using Cosmic Webhooks. Let’s get started.

Tools we’ll be using

Next.js - A React framework for production that makes it easy to spin up a full-stack application.
Cosmic - A Headless CMS enables the independence of the data (content) layer and gives us the ability to quickly manage website content.
Sass - A stable, and powerful professional-grade CSS extension language.

TL;DR

Check out the code
View the live demo
Install the App Template

https://imgix.cosmicjs.com/b94c8f60-c0ec-11ec-bf80-e74645a81647-template.gif

Quick Introduction

Next.js is a complete suite to build blazing fast React apps. It is developer-friendly and intuitive to use. With the release of Next.js 12.1, things are only going to get better with new features including performance optimization, middleware, React 18 support, on-demand ISR, expanded support for SWC, and more.
Cosmic is a great headless CMS that gives us the ability to fully manage and store our website content and media and update them quickly.

Explore 4 new killer features of Next.js and use them to template

Let's install a new Next.js app that includes tooling and configurations. For this tutorial, you'll need Node.js 12.22.0 or a later version. Open the terminal, paste or type

npx create-next-app@latest nextjs-restaurant-website-cms
# or
yarn create next-app nextjs-restaurant-website-cms

Install dependencies from the app folder:

cd nextjs-restaurant-website-cms
npm i
# or
cd nextjs-restaurant-website-cms 
yarn

You can now start the app in the development mode with:

npm run dev
# or
yarn dev

Open http://localhost:3000/ in your browser to see the ascetic home page.

1. Rust compiler

One of the key features of Next.js 12 is performance optimization. To boost performance, Next.js replaced the Babel compiler with an extensible Rust compiler and enabled it by default using Next.js 12, the compiler is built on top of — SWC — which stands up for Speedy Web Compiler. It does consume TypeScript/JavaScript and emits JavaScript code that can be executed on old browsers.

SWC is 20x faster than Babel on a single thread and 70x faster on four cores.

https://imgix.cosmicjs.com/92a157c0-be93-11ec-bf80-e74645a81647-Rust.png

2. Middleware

This is one of the most exciting features. Middlewares enable us to use code over configuration. This means you can run code before a request is completed, and based on the request, you can modify the response by rewriting, redirecting, adding headers, setting cookies, etc. With middlewares, you can implement things like authentication, bot protection, redirects and rewrites, server-side analytics, logging, and handling unsupported browsers, and more.

https://imgix.cosmicjs.com/d926ee80-be93-11ec-bf80-e74645a81647-Middleware.png

Middleware is created in /pages/_middleware.ts and it will run on all routes within the /pages directory. What does a _middleware.js file look like? Let’s see that through our template for example.

// pages/_middleware.js

import { NextResponse } from 'next/server';

export async function middleware( request ) {
  // create an instance of the class to access the public methods.
  //This uses next(),

  let response = NextResponse.next();

  const country = request.geo.country || 'US';
  const city = request.geo.city || 'San Francisco';
  const region = request.geo.region || 'CA';

  // get the cookies from the request
  let cookieFromRequest = request.cookies['location-cookie'];

  if(!cookieFromRequest) {
    // set the `cookie`
    response.cookie('location-cookie', `${country|city|region}`);
  }

  return response;
}

3. On-demand Incremental Static Regeneration ISR

Next.js now exposes a function unstable_revalidate() allowing you to revalidate individual pages that use getStaticProps. Inside getStaticProps, you don’t need to specify revalidate to use on-demand revalidation, only revalidate the page on-demand when unstable_revalidate() is called.

// pages/api/revalidate.js

export default async function handler(req, res) {
  try {
    await res.unstable_revalidate('/menu/' + req.body.data.slug)
    return res.json({ revalidated: true })
  } catch (err) {
    // If there was an error, Next.js will continue
    // to show the last successfully generated page
    return res.status(500).send('Error revalidating')
  }
}

4. Faster Image Optimization and Smaller images using AVIF

The built-in Image Optimization API has been updated to support the same pattern as ISR pages, where images are served stale and revalidated in the background. It also supports AVIF images enabling 20 percent smaller images compared to WebP.

This feature is opt-in and can be enabled by modifying the image. format property in the next.config.js file:

// next.config.js

const nextConfig = {
  reactStrictMode: true,
  images: {
        formats: ['image/avif', 'image/webp'],
        domains: ['imgix.cosmicjs.com'],
    },
}

module.exports = nextConfig

Cosmic Features overview

  • Customizable API: Build out the schema, models, and controllers for API from the editor. Cosmic offers both a REST and GraphQL API for our convenience.
  • Globally fast and secure content management system and API toolkit.
  • Webhooks Call back anywhere you need, to get the functionality you want, out of the box with Cosmic API.
  • Imgix Integration is included which allows you to do powerful image processing for dynamic apps optimized for cross-platform experiences.

Cosmic integration

The first step making a free Cosmic account. Let's select the "Start from scratch" option.

https://imgix.cosmicjs.com/cb3c32c0-be94-11ec-bf80-e74645a81647-cosmic1.png

Great! Now let's organize our content into groups, that share the same content model using Object Types. For example, you have sections with similar properties like section name, title, introduction, and picture and want to reuse this module to create content for different sections.

https://i.gyazo.com/ccef552e9b9a0161606c076163808316.png

Create Section Object Type and add section properties to define the "Metafields" in the "Content Model".

https://imgix.cosmicjs.com/72804b50-c0ef-11ec-bf80-e74645a81647-cosmic3.gif

Now you could create an Object Type model for sections and you can fill content like this.

https://imgix.cosmicjs.com/c91c93a0-c0f0-11ec-bf80-e74645a81647-cosmic5.gif

In a similar way, you can define modules and creates Object Type following the current data model, schema design

  • Singleton for a unique model,
  • Multiple reusable models.

Time to get values for the Next.js app

Install the Cosmic module to your Next.js app.

npm i cosmicjs
# or
yarn add cosmicjs

Then go to Cosmic Dashboard Your Bucket > Settings > API Access and find your Bucket slug and API read key.

https://imgix.cosmicjs.com/dabe7910-c0f1-11ec-bf80-e74645a81647-cosmic6.gif

Awesome! Add this Bucket slug and API read key into your Next.js app .env

//.env
COSMIC_BUCKET_SLUG=your_cosmic_slug
COSMIC_READ_KEY=your_cosmic_read_key

To use the template UI you need to clone it in GitHub. Open the terminal, paste or type this code to install all dependencies, and run it.

git clone https://github.com/cosmicjs/nextjs-restaurant-website-cms.git
cd nextjs-restaurant-website-cms

npm install
#or
yarn install

npm run dev
#or
yarn dev

Function getDataFromBucket request to the bucket that we create earlier in Cosmic Dashboard and get our created content from the Cosmic by the params type.

// src/lib/api.js

import Cosmic from 'cosmicjs';

const BUCKET_SLUG = process.env.COSMIC_BUCKET_SLUG
const READ_KEY = process.env.COSMIC_READ_KEY

const bucket = Cosmic().bucket({
  slug: BUCKET_SLUG,
  read_key: READ_KEY,
});

export async function getDataFromBucket(preview) {
  const params = {
    type: 'header',
    props: 'title,slug,metadata,created_at',
    sort: '-created_at',
    ...(preview && { status: 'all' }),
  }
  const data = await bucket.getObjects(params)
  return data.objects
}

Let's display our content, integrate it with our UI, and render some elements to the home page. For that, you need to add this to index.js.

// pages/index.js

import Head from 'next/head';
import Home from 'components/Home';
import Layout from 'components/Layout';
import Footer from 'components/Footer';
import AboutUs from 'components/AboutUs';
import SpacialMenu from 'components/Menu';
import Introduction from 'components/Introduction';
import VideoIntro from 'components/VideoIntro';
import Gallery from 'components/Gallery';
import Contacts from 'components/Contact';

import { getDataFromBucket } from 'lib/api';
import chooseByType from 'utils/chooseValueByType';

function Template({ data }) {
  return (
    <>
      <Head>
        <title>Next.js Restaurant CMS</title>
        <meta name="description" content="Create template using cosmic.js CMS" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Layout navbar={chooseByType(data, 'navigation')}>
        <Home info={chooseByType(data, 'header')}/>
        <AboutUs info={chooseByType(data, 'about')}/>
        <SpacialMenu info={[chooseByType(data, 'drink'), chooseByType(data, 'food')]}/>
        <Introduction info={chooseByType(data, 'history')}/>
        <Gallery info={[chooseByType(data, 'gallery'), chooseByType(data, 'food')]}/>
      </Layout>
      <Footer>
        <VideoIntro url={chooseByType(data, 'video')}/>
        <Contacts info={chooseByType(data, 'contact')}/>
      </Footer>
    </>
  )
}

export async function getStaticProps({ preview }) {
  const data = (await getDataFromBucket(preview)) || [];
  return {
    props: { data },
  }
}

export default Template;

The chooseByType function filters data by Object Type slug, which we created in the Cosmic Dashboard.

// src/utils/chooseValueByType.js

const chooseByType = (data, slugName) => {
  if( data && slugName ) {
    const chooseBySlug = data?.filter(content => Object.values(content).includes(slugName));
    return chooseBySlug ? chooseBySlug[0] : [];
  }
}

export default chooseByType;

Congrats you're almost there!

Making Menu Item Page

https://imgix.cosmicjs.com/cd50f1b0-c0f4-11ec-bf80-e74645a81647-cosmic7.gif

In Next.js you can create a dynamic route. For creating individual menu item page and dynamic route, consider the following page pages/menu/[slug].js:

// pages/menu/[slug].js

import Head from 'next/head';
import { useRouter } from 'next/router';

import Layout from 'components/Layout';
import Footer from 'components/Footer';
import Contacts from 'components/Contact';
import MenuIntro from 'components/MenuIntro';
import VideoIntro from 'components/VideoIntro';
import Gallery from 'components/Gallery';

import { getAllDataWithSlug,getDataFromBucket } from 'lib/api';
import chooseByType from 'utils/chooseValueByType';

function Menu({ data }) {
  const {
    query: {slug},
  } = useRouter();

  return (
    <>
      <Head>
        <title>Next.js Restaurant CMS</title>
        <meta name="description" content="Create template using cosmic.js CMS" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Layout navbar={chooseByType(data, 'navigation')}>
        <MenuIntro info={[chooseByType(data, 'food'), chooseByType(data, 'drink')]} slug={slug} />
        <Gallery info={[chooseByType(data, 'gallery'), chooseByType(data, 'food')]}/>
      </Layout>
      <Footer>
        <VideoIntro url={chooseByType(data, 'sushi')}/>
        <Contacts info={chooseByType(data, 'contact')}/>
      </Footer>
    </>
  )
}

export async function getStaticProps({ params, preview = null }) {
  const data = (await getDataFromBucket(preview)) || [];
  return {
    props: { data },
  }
}

export async function getStaticPaths() {
  const dataWithSlug = (await getAllDataWithSlug()) || [];

  return {
    paths: dataWithSlug.map((menu) => `/menu/${menu.slug}`),
    fallback: true,
  }
}

export default Menu;

getServerSideProps function is used to get the data from Cosmic each time this route is called. In pages/api/revalidate.js we use unstable_revalidate function to revalidate the page on-demand when unstable_revalidate() is called and if there was an error, Next.js will continue to show the last successfully generated page.

After deploying your codebase on Vercel you can enable the revalidation of content updates by going to the Cosmic Dashboard and navigating to Bucket Settings > Webhooks. The event to be triggered when editing content is object.edited.published . The Webhook URL endpoint will look like ${YOUR_VERCEL_DEPLOYMENT_URL}/api/revalidate.

This also makes it easier to update your site when content from your headless CMS is created or updated.

https://imgix.cosmicjs.com/422a0160-c1a0-11ec-bf80-e74645a81647-revalidate.gif

Time to test it out, edit your content in Cosmic Dashboard, and see the static content updated instantly!

https://imgix.cosmicjs.com/6ca59130-c1a4-11ec-bf80-e74645a81647-revalidate.gif

Conclusion

Congrats! Now you have a dynamic, customizable, and fully integrated template with new Next.js and Cosmic features, you can customize it for other types of businesses and use it how you prefer.

https://imgix.cosmicjs.com/b94c8f60-c0ec-11ec-bf80-e74645a81647-template.gif

Discover and read more posts from Naira
get started
post comments1Reply
Emmanuel DeLouise
2 years ago

This works for me, Thank you for this article