Создание клона сайта с CSS трюками с помощью Webiny и NextJS

Существует множество преимуществ использования безголовых систем управления контентом (CMS). Они часто становятся инструментом выбора при создании современных приложений. Существует множество различных CMS, которые решают проблемы, связанные с управлением контентом на разных платформах, но лишь некоторые из них обладают такими важными свойствами, как масштабируемость, конфиденциальность данных, эффективная стоимость эксплуатации и обслуживания, а также простота адаптации. Webiny реализует эти столь необходимые функции.

Цель

Цель этой статьи — познакомить вас с безголовыми CMS, а также научить вас создавать безсерверный блог, в данном случае CSS-трюки клона, используя Webiny и NextJS. Вы также узнаете о важности бессерверного подхода в построении современных приложений.

Введение в безголовую CMS

Безголовая система управления контентом, или headless CMS, — это система, работающая только с бэкендом и выполняющая в основном роль хранилища контента. Безголовая CMS делает контент доступным через API доставки контента (это может быть GraphQL или REST API) для отображения на любом устройстве без встроенного, фронтенда или презентационного слоя. Безголовая CMS позволяет подключить к хранилищу контента более одного презентационного слоя; это позволяет избежать трудностей, связанных с созданием и обслуживанием сервера.

Почему Webiny

Webiny позволяет вам самостоятельно размещать свои приложения на собственном облаке AWS, подчиняя свои данные собственным правилам и обеспечивая столь необходимую конфиденциальность данных. Это также снижает затраты на инфраструктуру. У вас есть возможность масштабировать свое приложение, когда вам это необходимо. Webiny также предлагает расширенные возможности, такие как экосистема плагинов, безопасность и инфраструктура как код.

Требования

Для выполнения этого проекта вам понадобятся:

  • Node.js: убедитесь, что на вашей машине установлен Node.js версии 14 или выше. Чтобы проверить версию Node.js на вашей машине, выполните node --version.
  • yarn ^1.22.0 || >=2: Webiny работает как на классической, так и на ягодной версии yarn.
  • Аккаунт AWS: Webiny позволяет вам самостоятельно разместить ваше приложение в облаке. Вы должны иметь действующую учетную запись AWS и учетные данные пользователя, установленные на вашей машине.

Установка и настройка нашего проекта Webiny

Давайте установим и настроим новый проект Webiny, чтобы начать сборку. Выполните эту команду в терминале

npx create-webiny-project css-tricks-clone
Войти в полноэкранный режим Выйти из полноэкранного режима

Следуйте инструкциям в терминале, это поможет вам:

  • Инициализировать папку проекта
  • настроить пряжу
  • установить пакет шаблонов
  • Инициализировать git

После этого вам

  • Вам будет предоставлено несколько опций для выбора региона AWS, в котором будет развернут ваш новый проект.
  • Выберите желаемую базу данных. На момент написания этой статьи Webiny поддерживает два типа: DynamoDB и DynamoDB + Elasticsearch. Вы должны выбрать ту, которая соответствует потребностям вашего проекта. Если вы планируете иметь проект небольшого или среднего размера, предпочтительной базой данных должна быть DynamoDB. В этом учебнике мы будем использовать DynamoDB.

После выполнения описанных выше действий Webiny автоматически установит необходимые зависимости для нашего проекта.

После создания проекта пришло время развернуть его в вашем аккаунте AWS. Для этого выполните следующую команду в терминале:

yarn webiny deploy

Войти в полноэкранный режим Выйти из полноэкранного режима

Эта команда сначала создаст проект вместе с необходимыми ресурсами облачной инфраструктуры. Первое развертывание может занять до 20 минут, вам нужно набраться терпения и дать процессу завершиться.

После этого вам будут представлены следующие URL-адреса:

➜ Main GraphQL API: это URL GraphQL API вашего проекта.

➜ Admin app: это административная область, где вы можете управлять всем в вашем хранилище контента.

➜ Публичный веб-сайт:

  • URL веб-сайта
  • URL предварительного просмотра сайта

В случае, если вы ошиблись с URL, вы всегда можете выполнить команду yarn webiny info в папке проекта Webiny, чтобы получить их.

Давайте перейдем к нашему URL admin app и настроим нашу Headless CMS, чтобы вы могли начать создание фронтенда.

  • После входа в систему нажмите New Content Model.

Давайте создадим нашу модель контента

Предоставьте модели контента необходимые записи — name, content model group и description.

Для целей данного руководства мы зададим для модели name значение «Блог» (название модели должно быть в единственном числе), для группы content model group — «Разгруппированные», а для description — «Клон CSS трюков».

  • Давайте вставим поля в нашу модель Blog. Ниже перечислены поля, которые мы будем использовать в проекте:
    • Заголовок: это заголовок вашего поста, тип поля будет text.
    • Post id: это number.
    • Тело: это содержательный текст с форматированием и ссылочным материалом.
    • Фото автора: тип поля — файл, разрешено только изображение.
    • Фотография содержимого: тип поля — file, разрешено только изображение.
    • Дата: это поле date для даты, когда была сделана запись в блоге.
    • Автор: тип поля — text.
    • тег: тип поля — text. Включите use as a list of texts для этого поля, потому что tag должен быть массивом.

Снова откройте меню и нажмите на Headless CMS > Ungrouped > Blog. Сделайте новые записи внутри модели — столько, сколько захотите.

Теперь, когда мы закончили с созданием и редактированием наших моделей, давайте перейдем в Settings, чтобы создать наш API ключ, а также получить токен. Этот токен понадобится нам для того, чтобы сделать запрос к headless CMS через конечную точку GraphQL.

Нажмите на «Настройки > API-ключи» и создайте новый ключ, который позволит получить доступ на чтение к безголовой CMS и файловому менеджеру. После нажатия кнопки «Сохранить» вам будет выдан новый ключ API. Скопируйте его и сохраните в безопасном месте. Мы будем использовать его для подключения нашего фронтенда.

Настройка API GraphQL

Перейдите на игровую площадку API, чтобы протестировать ваш API и убедиться, что все работает нормально. Чтобы перейти на площадку API, зайдите в админку и нажмите «API playground» на вкладке меню.

На площадке GraphQL Playground есть 3 вкладки, вам понадобится Headless CMS - Read API.

Давайте протестируем наш API, написав запрос для получения содержимого внутри нашей безголовой CMS:

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

    }
  }

}

Войти в полноэкранный режим Выйти из полноэкранного режима

Это должно дать нам все содержимое нашей безголовой CMS.

Я слышал, что вы сказали, что мы закончили с настройкой области администратора нашего проекта? О да! Вы угадали правильно.

Теперь давайте приступим к созданию фронтенда.

Создание приложения с помощью NextJS

Чтобы создать приложение NextJS, выполните команду . Обратите внимание, что приложение NextJS должно находиться на одном уровне с вашим проектом Webiny, а не внутри него.

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

Войдите в полноэкранный режим Выйти из полноэкранного режима

Наше приложение будет называться css-tricks-frontend.

Мы будем использовать graphql-request для выполнения API-запросов к нашей безголовой CMS. Давайте установим его:

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

Войдите в полноэкранный режим Выйти из полноэкранного режима

В качестве альтернативы вы можете использовать Apollo GraphQL или любую другую зависимость по вашему выбору для выполнения API-запросов.

После завершения установки мы можем приступить к написанию кода.

Структура папок

Важно сохранять код организованным для удобства чтения и сопровождения. Вот как выглядит папка нашего проекта NextJS.

.env.local: здесь будет храниться конечная точка GraphQL и токен CMS.

/lib/context.js: этот файл будет содержать логику нашего приложения.

/pages/header.js : заголовок приложения.

/pages/footer.js: нижний колонтитул приложения.

/pages/components/home.js: домашняя страница.

/pages/components/[post].js: этот маршрут будет представлять собой детали нашего поста.

/styles/App.css: CSS-файл.

Давайте начнем писать код.

.env.local

Этот файл используется для управления константами окружения нашего приложения. Мы будем хранить здесь секреты нашего приложения, а затем попросим git игнорировать этот файл. Это делается для того, чтобы наше приложение было защищено и не передавало секреты токенов на GitHub. Добавьте токен доступа и URL API, которые вы скопировали в предыдущих шагах.

NEXT_PUBLIC_TOKEN_SECRET= your_token_here
NEXT_PUBLIC_CMS_ENPOINT= your_CMS_enpoint_here
Войдите в полноэкранный режим Выход из полноэкранного режима

Код ниже — это логика нашего Context API. Context API позволяет обмениваться уникальными деталями и помогает решить проблему реквизитов со всех уровней вашего приложения. Мы можем получить доступ к данным из Context в любом месте нашего приложения.

Нам понадобится доступ к cmsData из других компонентов внутри нашего приложения.

/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;
Вход в полноэкранный режим Выход из полноэкранного режима

Внутри context.js, createContext() используется для правильного управления состоянием и повторного использования логики с состоянием в других компонентах нашего приложения.

callApi() вызывает конечную точку GraphQL нашего проекта через пакет graphql-request. Результат (или ответ) хранится внутри объекта состояния cmsData.

Давайте обернем ProductProvider вокруг всего нашего приложения, а также глобально импортируем стили.

/pages/_app.js

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

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

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

  )
}

export default MyApp
Вход в полноэкранный режим Выход из полноэкранного режима

/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
Вход в полноэкранный режим Выход из полноэкранного режима

Установите свойство background приложения, чтобы оно выглядело точно так же, как 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;

 }

Войти в полноэкранный режим Выйти из полноэкранного режима

Перейдите внутрь /pages/home.js давайте настроим нашу домашнюю страницу на отображение записей блога

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
Войдите в полноэкранный режим Выйти из полноэкранного режима

getParagraph дает абзац первого поста в хранилище контента. Это нужно нам для того, чтобы сделать первый пост больше остальных.

getFirstFivePosts — это первые пять постов в нашем хранилище контента, которые мы будем отображать в качестве дополнительного контента.

getOtherPosts — это все остальные посты в нашем хранилище контента.

Каждый из них был сопоставлен для отображения заголовка, тега, аватара автора, имени автора и даты публикации на нашей главной странице. Он также показывает небольшую часть содержимого поста.

Давайте воспользуемся динамическим маршрутом для получения и отображения выбранной записи блога.

[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
Вход в полноэкранный режим Выход из полноэкранного режима

RichTextRenderer — это компонент, который мы использовали для рендеринга насыщенного текста внутри нашего приложения. Webiny предоставляет этот пакет npm для рендеринга насыщенных текстов. Чтобы использовать эту зависимость, вам нужно установить ее с помощью следующей команды:

npm install --save @webiny/react-rich-text-renderer
Войти в полноэкранный режим Выйти из полноэкранного режима

Или, если вы предпочитаете yarn:

yarn add @webiny/react-rich-text-renderer
Войти в полноэкранный режим Выйти из полноэкранного режима

Компоненту RichTextRenderer передаются данные богатого текста для рендеринга, а затем он устанавливается внутри div post-content.

Перейдите на localhost:3000 для запуска приложения.
Ура!!! Наше приложение запущено и работает.

Вот полный код CSS для нашего приложения

/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;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Резюме

Мы создали клон CSS трюков

  • С Webiny Headless CMS для внутреннего проекта и мы создали модель контента для блога (CSS tricks).
  • Получили данные из Headless CMS в проект Next.js с помощью graphql-request.
  • Создали стилизацию для проекта и убедились, что она похожа на оригинальные трюки css.

Ура! Вы сделали это 🚀.

Смотрите код и живую демонстрацию

Оцените статью
devanswers.ru
Добавить комментарий