DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Emmanuel Aiyenigba
Emmanuel Aiyenigba

Posted on • Originally published at webiny.com

Building a CSS tricks website clone with Webiny and NextJS

webiny logo

There are many benefits of using a Headless Content Management Systems (CMS). They are often a tool of choice for building modern applications. There are a lot of different CMS that solve problems associated with managing content across platforms, but only a few come with important features such as scalability, data privacy, efficient cost of running & maintenance, and ease of adaption. Webiny implements these much-needed features.

Aim

This article aims at introducing you to headless CMS as well as teaching you how to build a serverless blog, in this case, CSS tricks clone using Webiny and NextJS. You will also get exposed to the importance of a serverless approach in building modern applications.

Introduction To Headless CMS

A Headless Content Management System, or headless CMS, is a backend-only system that acts primarily as a content repository. A headless CMS makes content accessible via a content delivery API (this could be a GraphQL or REST API) for display on any device without a built-in, frontend, or presentation layer. A headless CMS lets you connect more than one presentation layer to a content repository; this allows you to avoid going through the hurdles of building and maintaining a server.

Why Webiny

Webiny allows you to self-host your application on your own AWS cloud,Β bringing your data under your own rules, and providing you with that much-needed data privacy. This also reduces infrastructure costs. You have the liberty to scale your application when you need it. Webiny also offers advanced features such as a plugin ecosystem, security, and infrastructure as code.

Requirements

To follow this project you need:

  • Node.js: ensure to have Node.js version 14 or higher installed on your machine. To check the version of Node.js on your machine, run node --version
  • yarn ^1.22.0 || >=2: Webiny works on both the classic and berry versions of yarn
  • AWS account: Webiny allows you to self-host your application on the cloud. You must have a valid AWS account and user credentials set up on your machine.

Setting-Up and Configuring Our Webiny Project

Let's set up and configure a new Webiny project to begin building. Run this command inside your terminal

npx create-webiny-project css-tricks-clone
Enter fullscreen mode Exit fullscreen mode

Following the instructions in the terminal will help you:

  • Initialize project folder
  • setup yarn
  • install template package
  • Initialize git

Then you will:

  • Be provided with some options to choose the AWS region to which your new project will deploy to.
  • Select your database of choice. Webiny supports two types as at the time of writing this article: DynamoDB and DynamoDB + Elasticsearch. You are to pick the one that suits your project needs. If you intend to have a small to medium size project, DynamoDB should be your preferred database. We will use DynamoDB for this tutorial.

After the above steps, Webiny will automatically install the necessary dependencies for our project.

Once the project has been created, it's time to deploy it into your AWS account. To do this, run the following command in your terminal:

yarn webiny deploy

Enter fullscreen mode Exit fullscreen mode

This command will first build the project along with needed cloud infrastructure resources. The first deployment can take up to 20 minutes, you need to be patient and let the process finish.

When this in done, you will be presented with the following URLs:

➜ Main GraphQL API: this is the URL of your project's GraphQL API

➜ Admin app: this is an administrative area where you can control everything in your content repository.

➜ Public website:

  • Website URL
  • Website preview URL

Incase you misplace any of these URLs, you can always run yarn webiny info command in your Webiny project folder to get them.

Let's head over to our admin app URL and set-up our Headless CMS so that you we can start building the frontend.

  • Once you have logged in, click New Content Model

content model

Let's create our content model

create model

Provide the content model with the necessary entries - name, content model group and description.

model entry

For the purpose of this tutorial, we set the model name to "Blog" (Model name should be in singular form); the content model group to "Ungrouped" and the description to "CSS tricks clone".

  • Let's insert fields into our Blog model. The following are the fields we shall be using for the project:
    • Title: this is your post title and field type will be text
    • Post id: a number.
    • Body: this is a rich text with formatting and reference media.
    • Author's photo: field type is file and it will allow image only.
    • Content photo: field type is file and it will allow image only
    • Date: this is the date field for when a blog post was made.
    • Author: field type is text.
    • tag: field type is text. Enable use as a list of texts for this field, this is because tag is expected to be an array.

field

Open the menu again and click on Headless CMS > Ungrouped > Blog. Make new entries inside the model - as many as you want.

ungrouped

Now that we are done with creating and editing our models, let's go over to Settings to create our API key and also retrieve the token. We will need this token to make a request to the headless CMS via the GraphQL endpoint.

Click on Settings > API Keys and create a new key that allows read access to the Headless CMS and File Manager. Once you click β€œSave” you will be given a new API key. Copy that and keep it somewhere safe. We will use it to connect our frontend.

menu

token secret

Configuring The GraphQL API

Go to the API playground to test out your API and see if things are working fine. To navigate to the API playground, go to your admin area and click β€œAPI playground” on the menu tab.

There are 3 tabs in the GraphQL Playground, what you will need is the Headless CMS - Read API

read API

Let's test our API by writing a query to fetch the contents inside our headless CMS:

  {
    listBlogs{
    data{
      postId
      title
      body
      authorsPhoto
      contentPhoto
      date
      author
      tag

    }
  }

}

Enter fullscreen mode Exit fullscreen mode

This should give us all the content in our Headless CMS.

result

Did I hear you say we are done with setting-up our project admin area? Oh yes! You guessed right.

Now, let's begin building the frontend.

Building The Application With NextJS

To create a NextJS application, run. Note that, your NextJS application should be on the same level as your Webiny project and not inside it.

npx create-next-app@latest
# or
yarn create next-app
# or
pnpm create next-app

Enter fullscreen mode Exit fullscreen mode

The name of our application is css-tricks-frontend.

We shall be using graphql-request to make API request to our headless CMS. Let's install that:

 yarn add graphql-request
 #or
 npm install graphql-request

Enter fullscreen mode Exit fullscreen mode

Alternatively, you can use Apollo GraphQL or any dependency of your choice to make API request.

After installation is complete, we can begin writing our code.

Folder structure

It is important to keep your code organized for easy readability and maintainability. Here is what our NextJS project folder looks like.

.env.local: this will store the GraphQL endpoint and CMS token.

/lib/context.js: this file will contain our application logic.

/pages/header.js : application header.

/pages/footer.js: application footer

/pages/components/home.js: home page.

/pages/components/[post].js: this route will be our post details.

/styles/App.css: CSS file.

Let's begin writing some code

.env.local

This file is used for controlling our app’s environment constants. We would store our application secrets here and then ask git to ignore this file, this is so that we can keep our app secured by not pushing token secrets to GitHub. Please add the access token and API URL you copied in the previous steps.

NEXT_PUBLIC_TOKEN_SECRET= your_token_here
NEXT_PUBLIC_CMS_ENPOINT= your_CMS_enpoint_here
Enter fullscreen mode Exit fullscreen mode

The code below is our Context API logic. Context API enables you to exchange unique details and assists in solving prop-drilling from all levels of your application. We can access the data from Context anywhere within our application.

We will need to access cmsData from other components within our application.

/lib/context.js

import React, {createContext, useEffect, useState} from 'react';

import {GraphQLClient, gql} from 'graphql-request'

export const ProductContext = createContext();

const ProductProvider = ({children}) => {

    //state to store information from the headless cms
    const [cmsData, setCmsData] = useState({
        post: []
    })

    //useEffect to call graphql endpoint
useEffect(() =>{
     async function callApi(){
        const endpoint = process.env.NEXT_PUBLIC_CMS_ENPOINT

        const graphQLClient = new GraphQLClient(endpoint, {
            headers: {
                authorization: process.env.NEXT_PUBLIC_TOKEN_SECRET
            }
        })

        //query cms data

        const queryRequest = gql`
            {
                listBlogs{
                data{
                    postId
                    title
                    body
                    authorsPhoto
                    contentPhoto
                    date
                    author
                    tag

                }
            }

        }

        `
        const data = await graphQLClient.request(queryRequest)
        setCmsData({post: data.listBlogs.data})



     } 
     callApi()
    }, [])

    return(
       <ProductContext.Provider value={{
           ...cmsData

       }} >
           {children}
       </ProductContext.Provider>
    );
}

export default ProductProvider;
Enter fullscreen mode Exit fullscreen mode

Inside context.js, createContext() is used for proper state management and for reusing stateful logic inside other components of our application.

callApi() calls our project's GraphQL endpoint through the graphql-request package. The result (or response) is stored inside the cmsData state object.

Let's wrap ProductProvider around our entire application and also import styles globally.

/pages/_app.js

  import '../styles/App.css';
import {ProductProvider} from './context'

function MyApp({ Component, pageProps }) {
  return(
    <ProductProvider>

      <Component {...pageProps} />
    </ProductProvider>

  )
}

export default MyApp
Enter fullscreen mode Exit fullscreen mode

/pages/header.js

import React from 'react'
import Link from 'next/link'
function Header() {
  return (
    <div className='container-header'>

      <Link href="/">
          <div className='header'>
              <p>*</p>
              <h1>CSS-TRICKS |</h1>
              <p className='digitalocean'>DigitalOcean</p>    

          </div>
      </Link>
          <div className='nav-bar'>
            <p>Articles</p>
            <p>Videos</p>
            <p>Almanac</p>
            <p>Newsletter</p>
            <p>Guides</p>
            <p>DigitalOcean</p>
            <p>Docommunity</p>
            <p></p>
          </div>
    </div>
  )
}

export default Header
Enter fullscreen mode Exit fullscreen mode

Set the background property of the application to look exactly like css-tricks.com

/style/App.css

  :root{
    --featured-img: linear-gradient(180deg,#fff,#262626);
  }

 body{
    background-image: radial-gradient(50% 50% at top
    center,rgba(0,0,0,.66),#262626),var(--featured-img);

    background-size: 120% 2000px,100% 2000px;

 }

Enter fullscreen mode Exit fullscreen mode

Navigate inside /pages/home.js let's set our homepage to display blog posts

import React, {useContext} from 'react';
import {RichTextRenderer} from '@webiny/react-rich-text-renderer';
import {ProductContext} from '../../lib/context';
import Link from 'next/link';
import Image from 'next/image';

const Home = () => {

    const getPost = useContext(ProductContext)

    const getFirstFivePosts = getPost?.post.slice(0,5);
    const getOtherPosts = getPost?.post.slice(4);

    const getParagraph = getPost?.post[0]?.body.filter(post => post.type === "paragraph")?.slice(0,2);


  return (
    <div>
        <div>            
               <div>

                 {
                    getPost && getParagraph && 
                  <div className='firstPostContainer'>
                   <div className='image-div'>
                    <Image src={getPost?.post[0]?.contentPhoto} alt="avatar" 
                                    className='firstPostImage'
                                    width={500}
                                    height={300}
                                    layout="fill"
                         />

                            </div>


                            <div className='first-post'>

                            <p className='tag'>{getPost?.post[0]?.tag[0]}</p>
                            <Link href={'/components/' + getPost?.post[0]?.postId} >
                                <h2>{getPost?.post[0]?.title}</h2>


                            </Link>
                            <div className="post-intro firstPostIntro">
                                <RichTextRenderer  data={getParagraph}/>


                            </div>

                            <div className='firstPostAuthorInfo'>
                            {getPost?.post[0]?.authorsPhoto.length > 0 && (
                                 <Image src={getPost?.post[0]?.authorsPhoto} alt="avatar" className='avatar' width={40} height={40}
                                layout="fixed"
                                />

                                )}

                                <p className="author">{getPost?.post[0]?.author}</p>
                                <p className="date">{getPost?.post[0]?.date}</p>

                            </div>
                            </div>
                        </div>
                       }
                    </div>

        </div>
        <div className='aside-post-container'>
            {getFirstFivePosts && getFirstFivePosts.map(res =>{

                return(
                    <div key={res.postId} className="mini-card module">
                        <p className='article-date'>Article on {res.date}</p>
                        <Link href={'/components/' + res.postId}>

                            <h4>
                                {res.title}
                            </h4>
                        </Link>

                        <p className='aside-tag'>{res.tag[0]}</p>
                        <div className='author-info firstFourAvatar'>
                            <Image src={res.authorsPhoto} 
                            className="avatar" alt="avatar" width={40} 
                            height={40} layout="fixed" />
                            <p>{res.author}</p>

                        </div>
                    </div>
                )
            })}
        </div>

        <div className='card-container'>  

            {

                getOtherPosts && getOtherPosts.map(res =>{
                         // const getParagraph = getPost?.post[0]?.body.filter(post => post.type === "paragraph")?.slice(0,2);
                        const paragraph = res.body.filter(post => post.type === "paragraph")?.slice(0,2)    
                                return(

                                    <div className='card' key={res.postId}>
                                        <p className='tag'>{res.tag[0]}</p>
                                        <Link href={'/components/' + res.postId}>
                                            <h3>{res.title}</h3>

                                        </Link>

                                        <div className="post-intro">
                                            <RichTextRenderer data={paragraph}/>


                                         </div>
                                        <div className='author-info'>

                                            <Image src={res.authorsPhoto} alt="avatar" className='avatar' width={40} height={40}
                                            layout="fixed"
                                            />

                                            <p className="author">{res.author}</p>
                                            <p className="date">{res.date}</p>

                                        </div>

                                    </div>
                                )
                            })


            }
        </div>

    <div className="archive">
        <button className='button'> KEEP BROWSING IN THE ARCHIVES </button>

    </div>

    <div className='aside-post-container'>
            {getFirstFivePosts && getFirstFivePosts.map(res =>{
                return(
                    <div key={res.postId} className="mini-card module">
                        <p className='article-date'>Article on {res.date}</p>
                        <Link href={'/components/' + res.postId}>

                            <h4>
                                {res.title}
                            </h4>
                        </Link>

                        <p className='aside-tag'>{res.tag[0]}</p>
                        <div className='author-info firstFourAvatar'>
                            <Image src={res.authorsPhoto} 
                            className="avatar" alt="avatar" width={40} 
                            height={40} layout="fixed" />
                            <p>{res.author}</p>

                        </div>
                    </div>
                )
            })}
        </div>
    </div>
  )
}

export default Home
Enter fullscreen mode Exit fullscreen mode

getParagraph gives the a paragraph of the first post inside the content repository. We need this so we can make the first post bigger than the others.

getFirstFivePosts is the first five posts in our content repository which we shall be displaying as an aside content.

getOtherPosts is every other posts in our content repository.

Each of these was mapped to display the title, tag, author's avatar, author's name and date of post on our home page. It also shows a little of the post content.

Let’s use dynamic route to get and display selected blog post.

[post].js

import React, {useContext, useEffect, useState} from 'react'
import Image from 'next/image';
import {useRouter} from 'next/router';
import {GraphQLClient, gql} from 'graphql-request'
import Header from './header';
import { RichTextRenderer } from '@webiny/react-rich-text-renderer';

function Post() {
    const [getPost, setGetPost] = useState()

    const router = useRouter()

    const {post} = router.query


    useEffect(() =>{
        async function callApi(){

            const endpoint = process.env.NEXT_PUBLIC_CMS_ENPOINT

           const graphQLClient = new GraphQLClient(endpoint, {
               headers: {
                   authorization: process.env.NEXT_PUBLIC_TOKEN_SECRET
               }
           })

           //query cms data

           const queryRequest = gql`
                query getBlog($post: String) {
                   listBlogs(where: {
                    postId: $post
                   }){
                   data{
                       postId
                       title
                       body
                       authorsPhoto
                       contentPhoto
                       date
                       author
                       tag

                   }
               }

           }

           `
           const variables = {
            post: post
           }
           const data = await graphQLClient.request(queryRequest, variables)
           setGetPost(data.listBlogs.data)
           data.listBlogs.data.map(res => setGetPost(res))




        } 
        callApi()
    }, [post])


     return(

        <div className="container" >

        <Header />

       {getPost && (
        <div>
        <p className='tag'>{getPost?.tag[0]}</p>
        <h1 className='title'>{getPost?.title}</h1>

        <div className="author-bio author-info">
            <Image src={getPost.authorsPhoto} alt="avatar" className='avatar' 
            width={40} height={40} layout="fixed" />
            <p className='author'>{getPost.author}</p>
            <p className='date'>{getPost.date}</p>
        </div>

        <div className="article-sponsor">
            <p>DigitalOcean joining forces with CSS-Tricks! Special welcome offer: get $100 of free credit.</p>
        </div>
        <div className="post-content">
            <RichTextRenderer data={getPost?.body} />     
        </div>
        </div>
        )}

    </div>

   with  

    )
}

export default Post
Enter fullscreen mode Exit fullscreen mode

RichTextRenderer is a component we used to render rich text inside our application. Webiny provides this npm package for rendering rich texts. To use this dependency, you need to install it using the following command:

npm install --save @webiny/react-rich-text-renderer
Enter fullscreen mode Exit fullscreen mode

Or if you prefer yarn:

yarn add @webiny/react-rich-text-renderer
Enter fullscreen mode Exit fullscreen mode

The RichTextRenderer component is passed the rich text data to render and then mounted inside the post-content div.

Go ahead to localhost:3000 to run the application.
Yay!!! We have our application up and running.

Here is the complete CSS code for our application

/styles/App.css

@import url('https://fonts.googleapis.com/css2?family=Cantarell&family=Lato&family=Open+Sans:wght@300&family=Oxygen:wght@300&family=Roboto:wght@100&family=Rubik:wght@500&family=Ubuntu:wght@300&display=swap');

:root{
    --featured-img: linear-gradient(180deg,#fff,#262626);
}

body{
    background-image: radial-gradient(50% 50% at top center,rgba(0,0,0,.66),#262626),var(--featured-img);
    background-size: 120% 2000px,100% 2000px;

}

.header{
    color: white;
    cursor: pointer;
    display: flex;
    margin-bottom: -5rem;

}
.header p{
    font-size: 4rem;
    margin-top: -.01rem;

}
.header .digitalocean{
    font-size: 1.1rem;
    margin-top: 1.05rem;
    padding-left: .2rem;
    font-family: 'Rubik', sans-serif;
}

.header h1{
    margin-top: .8rem;
    font-size: 1.6rem;
    font-family: Montserrat;
    font-weight: 300;

}
.nav-bar{
    display: flex;
    color: #fff;
    overflow-x: scroll;
    scrollbar-color: #5e5e5e;
}

.nav-bar p{
    text-transform: uppercase;
    padding: 0 .7rem;
    font-family: 'MD Primer Bold', Rubik, Lato, 'Lucida Grande', 'Lucida Sans Unicode',Tahoma,Sans-Serif;
    font-size: .6rem;
    font-weight: 600;
}
.container-header{
    background: #111111;
    margin: -.5rem -.5rem 3rem -.5rem;

}

@media(min-width: 1240px){
    .container-header{
        display: flex;
        margin-bottom: 4rem;
    }
    .nav-bar p{
        font-size: .8rem;
        margin-top: 1.28rem;
        padding-left: 2rem;
    }
    .nav-bar{
        overflow-x: hidden;
    }


}

.first-post{
    border: 1px solid black;
    margin-bottom: 1.4rem;
    box-sizing: border-box;
    width: 97%;

    padding: 0 .3rem;
    word-wrap: break-word;
    border-radius: 15px;
    margin: auto;
    margin-top: -5rem;
    background: #fff;
    position: relative;

}
.image-div{
    box-sizing: border-box;
    width: 97%;
    margin: auto;
    margin-top: 5rem;
    height: 16rem;
    padding: 0 .2rem;
    position: relative;
    opacity: 0.8;
    z-index: -1;
    cursor: pointer;
}
.firstPostImage{
    z-index: -1;
    border-radius: 15px;
}

@media(min-width: 800px){
    .image-div{
       margin-top: 2rem;
       width: 97%;
       height: 30rem;
       border-radius: 8px;
       opacity: 0.7;
       margin-right: -3rem;
       z-index: -1;
    }

    .first-post{
        /* z-index: 1; */
        opacity: 1;
        margin-top: 2rem;
        height: 30rem;
        border-radius: 8px;
    }
    .firstPostContainer{
        margin-top: -1rem;
        display: flex;
        padding: 0 1.5rem;
    }
}

@media (min-width: 1240px){
    .image-div{
        height: 28rem;

    }

    .first-post{
        height: 28rem;
    }
    .firstPostIntro{
        padding-bottom: 0;
    }


}
.first-post h2{
    font-size: 2.5rem;
    padding-left: 1.4rem;
    cursor: pointer;
    /* font-family: Blanco, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; */
    font-family: 'Rubik', sans-serif;
}
.post-intro{
    margin: -1.3rem 0 1.2rem;
    font-size: .9rem;
    padding: .2rem 1rem .2rem 1rem;
    font-family: 'Oxygen', sans-serif;


}
.avatar{
    width: 40px;
    height: 40px;
    border-radius: 50%;
    margin-right: 0.5rem;
}
.tag{
    color: #ff7a18;
    padding-left: 1.2rem;
}
.author-info{
    display: flex;
    margin-left: .9rem;

}
.author{
    font-family: 'Rubik', sans-serif;
    font-weight: 800;
}
.author-info p{
    padding-left: .5rem;
    font-size: .9rem;
    font-family: 'Oxygen', sans-serif;

}
.date{
   padding-left: .4rem; 
}

.card1{
    border: 2px solid black;
    margin-bottom: 1.4rem;
    box-sizing: border-box;
    width: max-content;
    padding: 0 .3rem;
    height: 8rem;
}

/* aside posts */

.mini-card {
    width: 14em;
    height: 18em;
    box-shadow: -2rem 0 3rem -2rem #000;
    padding: 1.5rem;
    border-radius: 16px;
    background: linear-gradient(85deg,#434343,#262626);
    color: #fff;
    position: relative;
    /* transition-property: transform; */
    transition-duration: .5s;
    transform: none;

}
.module {
    margin: 1rem -2rem 2rem;

}
.mini-card h4{
    font-size: .9rem;
    cursor: pointer;
    font-family: 'MD Primer Bold', Rubik, Lato, 'Lucida Grande', 'Lucida Sans Unicode',Tahoma,Sans-Serif;

}
.mini-card:hover{
    transform: rotate(12deg);
    margin-right: 1.2rem;

}
.firstFourAvatar{
    position: absolute;
    bottom: 0;
}
.firstFourAvatar p{
    font-family: 'Rubik', sans-serif;
    font-weight: 500;
    font-size: .8rem;
}
.aside-post-container{
    display: flex;
    margin-left: 4rem;   


}
.aside-tag{
    color: #ff7a18;
    font-size: .6rem;
    font-family: 'Oxygen', sans-serif;
    font-weight: 500;
}
.mini-card h3{
    font-size: 1rem;
    cursor: pointer;
}
.article-date{
    font-size: .7rem;
    font-family: 'Oxygen', sans-serif;
}
.card{
    border: 2px solid black;
    margin-bottom: 1.4rem;
    box-sizing: border-box;
    width: 97%;
    /* height: 80%; */
    padding: 0 .3rem;
    word-wrap: break-word;
    border-radius: 15px;
    margin: auto;
    margin-top: 2rem;
    background: #fff;
}

.card h3{
    font-size: 1.3rem;
    padding-left: 1.4rem;
    cursor: pointer;
    font-family: 'MD Primer Bold', Rubik, Lato, 'Lucida Grande', 'Lucida Sans Unicode',Tahoma,Sans-Serif;
    font-weight: 600;
}
@media (min-width: 711px){
    .card{
        width: 40%;

    }
    .card-container{
        display: flex;
        flex-direction: row;
        flex-wrap: wrap;
        justify-content: space-between;
    }
}

.container .tag{
    color: #ffdfc7;
    font-family: 'Oxygen', sans-serif;
    margin-top: -1.1rem;
}

.container .title{
    color: #fff;
    margin-top: -.9rem;
    margin-left: 1rem;
    font-size: 2rem;
    width: 90%;
    font-family: 'Rubik', sans-serif;

}
.author-bio{
    color: #fff;
    margin-top: 1.5rem;
}

.post-content{
    border: 1px solid #fff;
    width: 90%;
    border-radius: 16px;
    background-color: #fff;
    margin: auto;
    word-wrap: break-word;
    padding-bottom: 2rem;
    margin-bottom: 3rem;
    padding: .6rem;
    font-size: .9rem;
    font-family: 'Oxygen', sans-serif;
}
.text-prop{
    padding: .4rem 1rem;
}

.article-sponsor{
    padding: .5rem 1.5rem calc(1rem + 10px);
    border-radius: 8px;
    background: rgba(0,0,0,.2);
    color: rgb(235, 227, 227);
    width: 95%;
    margin: auto;
    margin-bottom: -1.4rem;
    margin-top: 1rem;
}
.archive .button{
    margin-top: 3rem;
    font-family: MD Primer Bold,Rubik,Lato,Lucida Grande,Lucida Sans Unicode,Tahoma,Sans-Serif;
    font-style: normal;
    background-color: #5e5e5e;
    color: #fff;
    border: 0;
    border-bottom: 1px solid #262626;
    border-right: 1px solid #262626;
    box-shadow: inset 1px 1px 0 #777, 0 2px 3px rgb(0 0 0 / 40%);
    display: inline-block;
    font-weight: 700;
    line-height: 1.4;
    text-transform: uppercase;
    text-decoration: none;
    border-radius: 4px;
    white-space: nowrap;
    padding: 1rem 1.5rem;
    text-align: center;
    transition: .07s;
    position: relative;
    width: 65%;
    margin-left: 1.5rem;
    margin-bottom: 2rem;
}

/* footer */
.footer-container{
    font-family: 'Oxygen', sans-serif;
    background: #111111;
    margin-top: 2rem;
    margin: -.5rem -.5rem 0 -.5rem;
}
.footer-container h5{
    font-family: 'Oxygen', sans-serif;
    color: #ff7a18
}
.footer-elements-container{
    color: #a9a39f;
    margin: 0 1rem;
    display: flex;
    justify-content: space-between;
    flex-wrap: wrap;

}
.container4{
    padding-top: 2.7rem;
}
.poweredby{
    color: #fff;
    padding-top: 3rem;
    padding-left: 1.5rem;
    font-size: 1.5rem;
    padding-bottom: 2.5rem;
    box-sizing: border-box;
    margin-top: 6rem;

}
.move{
 margin-right: 4rem;   
}
.footer-container span{
    color: #0089c7;
}
@media (min-width: 952px){
    .poweredby{
        width: 30%;
        box-sizing: border-box;
    }
}

@media (min-width: 1200px){
    .container .title{
        font-size: 3.5rem;
        width: 70%;
        word-wrap: break-word;

    }
}

.firstPostAuthorInfo{
    display: flex;
    margin-left: .9rem;
    position: absolute;
    bottom: 0;

}
.firstPostAuthorInfo p{
    padding-left: .5rem;
    font-size: .9rem;

}
.firstPostIntro{
    padding-bottom: 2rem;
}
Enter fullscreen mode Exit fullscreen mode

Summary

We have created CSS tricks clone

  • With Webiny Headless CMS for the back-end project and we created the content model for the blog (CSS tricks).
  • Fetched the data from the Headless CMS to the Next.js project using graphql-request.
  • Created styling for the project and ensured that it looks similar to the original css tricks

hurrayyyy

Yay! You did it πŸš€.

See code and live demo

Top comments (2)

Collapse
 
endymion1818 profile image
Ben Read

Fantastic article @emmanuelthecoder, thanks for sharing!

Collapse
 
emmanuelthecoder profile image
Emmanuel Aiyenigba

I'm glad you find it helpful, @endymion1818

Create an Account! The only reason people scroll to the bottom... Β 
is because they want to read more.

Create an account to bookmark, comment, and react to articles that interest you.