Создание портфолио разработчика с помощью Next.js и Cosmic

Как разработчик, одна из самых ценных вещей, которую вы можете сделать, — это создать свое присутствие в Интернете. Ваше портфолио — это отличное место для демонстрации ваших проектов обществу или потенциальным работодателям.

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

Инструменты, которые мы будем использовать

Для создания портфолио разработчика мы будем использовать следующие технологии:

  • Next.js — фреймворк React, позволяющий легко создать полнофункциональное приложение.
  • Cosmic — Headless CMS обеспечивает независимость слоя данных (контента) и дает нам возможность быстро управлять содержимым шаблонов. В данном случае это наш блог и посты проекта.
  • Tailwind CSS — производительный CSS-фреймворк, ориентированный на утилиты, который можно компоновать непосредственно в разметке.

Основные моменты

Хотя основная цель этого портфолио — быть функциональным и показать, кто вы как разработчик, я хотел убедиться, что мы отдали приоритет пользовательскому опыту и общей производительности сайта.

  • Организованное управление контентом — С Cosmic мы можем хранить весь наш контент в одном месте. Как только мы напишем код для нашего пользовательского интерфейса, нам не придется беспокоиться о хранении контента внутри нашего проекта. Cosmic позаботится обо всем этом.
  • Оптимизация изображений Next.js — Благодаря возможностям Next Image мы можем быть уверены, что наши изображения не будут замедлять нас ни на йоту. Храня локальные изображения (если вы предпочитаете их включать), а также удаленные изображения, которые мы будем запрашивать из нашего Cosmic bucket, мы будем использовать такие вещи, как ленивая загрузка, размытие подложки и встроенная оптимизация изображений Next.js.
  • Лучшие практики SEO и доступности — Как веб-разработчику, вам крайне важно соблюдать хорошую семантику, чтобы ваш сайт был доступен для всех.

TL;DR

Установите шаблон

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

Проверьте код

Начиная с пустого приложения Next.js

Чтобы начать работу с этим шаблоном, давайте создадим новое приложение Next.js.

pnpx create-next-app@latest nextjs-developer-portfolio
# or
yarn create next-app nextjs-developer-portfolio
# or
npx create-next-app@latest nextjs-developer-portfolio
Вход в полноэкранный режим Выйдите из полноэкранного режима

Затем установите зависимости.

cd nextjs-developer-portfolio
pnpm install
# or
cd nextjs-developer-portfolio 
yarn
# or
cd nextjs-developer-portfolio 
npm install

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

Давайте запустим наше приложение! После выполнения приведенной ниже команды вы можете открыть http://localhost:3000 в своем браузере.

pnpm install
# or
yarn install
# or
npm install
Войти в полноэкранный режим Выход из полноэкранного режима

Начало работы с Cosmic

Прежде всего, давайте создадим бесплатный аккаунт Cosmic. После его создания мы можем приступить к созданию нового проекта. Выберите «Начать с нуля», затем вы можете назвать свой проект. Поскольку это наше основное ведро, в котором мы будем создавать и развертывать наш проект, я назову среду ведра «Production». Идем дальше и выбираем «Сохранить ведро».

Далее мы можем начать добавлять объекты в наше Cosmic Bucket.

Модель содержимого

Модель содержимого — это схема нашего объекта. Она состоит из данных, которые могут быть простыми, как одно текстовое значение, или сложными, как хранение нескольких значений данных. Это могут быть строки, числа, булевы и т.д. Суть настройки этой модели контента заключается в том, чтобы каждый раз, когда мы создаем новую запись в блоге, все Metafields, которые мы создали в нашем чертеже, были там для заполнения.

Чтобы добавить наши пользовательские метаполя в модель контента, мы можем нажать на символ плюса и добавить новое метаполе. Затем нам будет предложено выбрать из списка типов метаполей.

Объект Categories

Давайте создадим наш объект Categories. Единственным элементом данных, который нам понадобится для объекта категорий, будет заголовок, поэтому нам не нужно добавлять никаких метаполей.

Объект Posts

Вот как будет выглядеть наша модель контента для объекта Posts. Метаполя будут следующими:

  • Категория — Мы свяжем его с нашим объектом Post Categories. Тип: Однообъектное отношение.
  • Изображение обложки — Изображение, которое мы можем отобразить в верхней части поста. Тип: Изображение / Файл.
  • Отрывок — короткое предложение, резюмирующее наше сообщение. Тип: Обычный текстовый ввод.
  • Содержание — Текст, который будет содержаться в нашем сообщении. Тип: Markdown.

Обратите внимание, что по умолчанию, когда мы создаем новый объект, он будет иметь поле content и slug. Мы будем использовать slug (который Cosmic генерирует для нас) в нашем коде для правильной маршрутизации наших постов.

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

Объект Works

Мы также создадим объект «Работы» для демонстрации наших лучших проектов. Скопируйте чертеж объекта «Posts», но добавьте два дополнительных Metafields. Это будут:

  • Repo URL — ссылка на GitHub-репозиторий проекта. Тип: «Plain Text Input».
  • Live URL — ссылка на реальный веб-сайт вашего проекта. Тип: «Plain Text Input».

Установка модуля Cosmic NPM

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

cd nextjs-developer-portfolio
pnpm install cosmicjs
# or
npm install cosmicjs
# or
yard add cosmicjs
Вход в полноэкранный режим Выход из полноэкранного режима

Установка переменных окружения

Нам потребуется создать три переменные окружения внутри файла .env в корне нашего проекта. Bucket Slug и Read Key можно найти на вашей приборной панели в Settings > API Access. Ключ предварительного просмотра — это то, что вы можете определить сами, поэтому создайте свой собственный секретный ключ предварительного просмотра, чтобы использовать его в дальнейшем.

// nextjs-developer-portfolio/.env

COSMIC_BUCKET_SLUG=<your_bucket_slug>
COSMIC_READ_KEY=<your_read_key>
COSMIC_PREVIEW_SECRET=<your_preview_secret>
Вход в полноэкранный режим Выход из полноэкранного режима

Получение постов

Теперь, когда мы установили переменные окружения, мы готовы получить доступ к Cosmic API и получить наши данные.

// nextjs-developer-portfolio/src/lib/cosmic.js

const Cosmic = require('cosmicjs')
const api = Cosmic()

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,
})

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

Получая наши сообщения, мы также можем создать несколько параметров. В нашем примере получение наших «Posts» и «Works» будет одной и той же функцией, хотя мы передадим аргумент при вызове функции, объявляющий, какой объект нужно получить. Мы можем сделать это и для наших категорий, передав в качестве аргумента название нашего объекта Cosmic.

Чтобы лучше понять, как мы получаем данные из Cosmic, давайте разделим их на части:

  • Запрос — отправка правильных JSON-запросов на конечные точки Object и Media. Полное руководство вы можете посмотреть здесь.
  • Статус — если не включен, статус по умолчанию будет опубликован. Вы можете включить как опубликованное, так и черновое содержимое, установив статус на any.
  • Реквизиты — используются для объявления только необходимых данных и ограничения размера полезной нагрузки.
  • Limit — количество возвращаемых объектов.
  • Сортировать — сортировка содержимого.
// nextjs-developer-portfolio/src/lib/cosmic.js

export async function getAllPosts(preview, postType, postCount) {
  const params = {
    query: { type: postType },
    ...(preview && { status: 'any' }),
    props:
      'title,slug,metadata.category,metadata.excerpt,metadata.published_date,created_at,status',
    limit: postCount,
    sort: '-created_at',
  }
  const data = await bucket.getObjects(params)
  return data.objects
}
Вход в полноэкранный режим Выход из полноэкранного режима

Разбор разметки

Поскольку мы будем писать наш контент в формате Markdown, нам понадобится способ сериализовать его в HTML. Для этого мы установим зависимости remark и remark-html.

pnpm install remark remark-html
// or
yarn add remark remark-html
// or
npm install remark remark-html 
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь, когда мы установили эти пакеты, создайте файл в папке lib вашего проекта.

// nextjs-developer-portfolio/src/lib/markdownToHtml.js
import { remark } from 'remark'
import html from 'remark-html'

export default async function markdownToHtml(markdown) {
  const result = await remark().use(html).process(markdown)
  return result.toString()
}
Вход в полноэкранный режим Выход из полноэкранного режима

Создание списка постов

Теперь, когда мы выполнили базовую настройку нашего Cosmic bucket, создали файл .env с необходимыми переменными окружения, создали функциональность для получения данных и разобрали наш markdown, мы можем создать список постов, чтобы пользователи могли выбирать из них.

Для нашего поста lit мы отобразим заголовок и отрывок из поста. Вот как это будет выглядеть:

Мы можем создать компонент «PostList.jsx», чтобы мы могли с легкостью повторно использовать наш список постов в нескольких частях нашего сайта. Когда мы отобразим этот компонент на одной из наших страниц, мы передадим данные всех постов, полученных от Cosmic, в параметр «allPosts».

// nextjs-developer-portfolio/src/components/PostList.jsx

import Link from 'next/link'

const PostList = ({ allPosts, postType }) => {
  return (
    <>
      <ul className="grid grid-cols-1 md:grid-cols-2 gap-8">
        {allPosts.map(post => (
          <li
            className="flex flex-col bg-white dark:bg-gray-800 rounded p-8 shadow-sm"
            key={post.title}
          >
            <Link href={`/${postType}/${post.slug}`}>
              <a className="group flex flex-col justify-center gap-y-6">
                <div className="max-w-lg">
                  <h3 className="text-xl font-bold mb-1 group-hover:text-accent transition-colors">
                    {post.title}
                  </h3>
                  <p className="text-fore-subtle mb-3 lg:mb-0 lg:pr-6">
                    {post.metadata.excerpt}
                  </p>
                </div>
                <p className="flex items-center text-fore-subtle text-sm">
                  Read more
                </p>
              </a>
            </Link>
          </li>
        ))}
      </ul>
    </>
  )
}
export default PostList
Вход в полноэкранный режим Выход из полноэкранного режима

Рендеринг списка постов

Теперь давайте возьмем этот список постов и отобразим его на странице «Posts». Если вы еще не сделали этого, создайте папку «posts» в папке «pages» в вашем каталоге. Затем создадим индексный файл для этой страницы, где будет находиться наш PostList.

С помощью getStaticProps мы вызовем функции, созданные нами ранее, чтобы получить эти посты из Cosmic. Самое замечательное в этом то, что когда наступит время сборки, эти посты будут созданы статически и развернуты на граничной CDN, что сделает страницы доступными для пользователей по всему миру в течение миллисекунд.

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

// nextjs-developer-portfolio/pages/posts/index.jsx

import { useState } from 'react'
import { getAllPosts, getAllCategories } from '@/lib/cosmic'
import PostList from '@/components/PostList'

const Posts = ({ allPosts, allPostCategories }) => {
  const [filterCategory, setFilterCategory] = useState('All')

  const filteredPosts = allPosts.filter(
    post => post.metadata.category.title === filterCategory
  )

  return (
    <>
      <h1 className="text-2xl md:text-3xl text-fore-primary font-bold">
        Posts
      </h1>
      <ul className="flex flex-wrap gap-y-2 sm:gap-y-0 gap-x-4 my-4">
        <li
          className={
            'All' === filterCategory
              ? 'cursor-pointer font-bold filter--active transition'
              : 'cursor-pointer text-fore-subtle transition'
          }
          onClick={() => setFilterCategory('All')}
          key={'All'}
        >
          All
        </li>
        {allPostCategories.map(category => (
          <li
            className={
              category.title === filterCategory
                ? 'cursor-pointer font-bold filter--active transition'
                : 'cursor-pointer text-fore-subtle transition hover:text-accent'
            }
            onClick={() => setFilterCategory(category.title)}
            key={category.title}
          >
            {category.title}
          </li>
        ))}
      </ul>
      <PostList
        allPosts={filterCategory === 'All' ? allPosts : filteredPosts}
        postType="posts"
      />
    </>
  )
}

export async function getStaticProps({ preview }) {
  const allPosts = (await getAllPosts(preview, 'posts')) || []
  const allPostCategories = (await getAllCategories('post-categories')) || []
  return {
    props: { allPosts, allPostCategories },
  }
}
export default Posts
Вход в полноэкранный режим Выход из полноэкранного режима

Создание страницы отдельного поста

В папке «posts» (pages/posts) создадим файл [slug].jsx. Здесь мы можем написать код для наших отдельных постов.

Содержимое страницы поста будет состоять из трех компонентов:

  • PostHeader — содержит заголовок поста, метаданные поста (дата и категория) и изображение обложки.
  • PostTitle — название поста
  • PostContent — стилизованный HTML, который мы преобразовали из Markdown.
  • markdown-styles.module.css — таблица стилей для нашего уцененного текста.
/* nextjs-developer-portfolio/src/components/markdown-styles.modules.css */

.markdown {
  @apply text-lg leading-relaxed;
}

.markdown p,
.markdown ul,
.markdown ol,
.markdown blockquote {
  @apply my-6 text-fore-secondary text-[16px] md:text-[18px];
}

.markdown h2 {
  @apply text-fore-primary text-2xl font-bold mt-12 mb-4 leading-snug;
}

.markdown h3 {
  @apply text-fore-primary text-xl font-bold mt-8 mb-4 leading-snug;
}

.markdown p a {
  @apply text-accent underline hover:text-opacity-70;
}

.markdown ul li {
  @apply list-disc list-inside mb-2 bg-back-subtle p-2 rounded text-[16px] md:text-[18px] font-semibold;
}

.markdown ol li {
  @apply list-decimal list-inside mb-2 bg-back-subtle p-2 rounded text-[16px] md:text-[18px] font-semibold;
}

.markdown img {
  @apply max-w-[xl] mx-auto my-12;
}

Вход в полноэкранный режим Выход из полноэкранного режима
// nextjs-developer-portfolio/src/components/PostHeader.jsx
import Date from './Date'
import CoverImage from './CoverImage'
import PostTitle from './PostTitle'
import { ExternalLinkIcon } from '@/configs/icons'
import Image from 'next/image'
import avatar from '../../public/images/avatar_4.png'

const PostHeader = ({ post }) => {
  return (
    <>
      <PostTitle>{post.title}</PostTitle>
      <div className="flex items-center mb-8">
        <div className="flex items-center relative">
          <Image
            src={avatar}
            width={42}
            height={42}
            alt="Stefan Kudla"
            className="rounded-full"
            placeholder="blur"
          />
          <span className="ml-2 text-sm">
            Stefan Kudla |{' '}
            <Date dateString={post.created_at} formatStyle="LLLL dd, yyyy" /> |{' '}
            {post.metadata.category.title}
          </span>
        </div>
      </div>
      <CoverImage
        title={post.title}
        url={post.metadata.cover_image.imgix_url}
      />
      <div className="flex flex-row justify-between sm:items-center pb-8 border-b">
        <div className="sm:flex items-center gap-x-2">
{/* For our "Works" page that contains the "liveURL" and "gitHubURL" metafields. */}
          {post.metadata.live_url ? (
            <>
              <a
                href={post.metadata.live_url}
                target="_blank"
                rel="noreferrer"
                className="flex items-center text-accent hover:text-gray-500 text-sm md:ml-4 w-fit"
              >
                Live Site
                <span>
                  <ExternalLinkIcon />
                </span>
              </a>

              <a
                href={post.metadata.repo_url}
                target="_blank"
                rel="noreferrer"
                className="flex items-center text-accent hover:text-gray-500 text-sm"
              >
                Github Repo
                <span>
                  <ExternalLinkIcon />
                </span>
              </a>
            </>
          ) : undefined}
        </div>
      </div>
    </>
  )
}
export default PostHeader

// src/components/PostTitle.jsx
const PostTitle = ({ children }) => {
  return (
    <h1 className="text-fore-primary text-3xl sm:text-4xl md:text-5xl font-bold tracking-normal leading-tight md:leading-none mb-12 mt-4">
      {children}
    </h1>
  )
}
export default PostTitle

// src/components/PostContent.jsx
import markdownStyles from './markdown-styles.module.css'

const PostBody = ({ content }) => {
  return (
    <div className="max-w-2xl mx-auto">
      <div
        className={markdownStyles['markdown']}
        dangerouslySetInnerHTML={{ __html: content }}
      />
    </div>
  )
}
export default PostBody
Войти в полноэкранный режим Выйти из полноэкранного режима

Сама страница:

// nextjs-developer-portfolio/src/pages/posts/[slug].jsx

import { useRouter } from 'next/router'
import PostBody from '@/components/PostBody'
import PostHeader from '@/components/PostHeader'
import { getAllPostsWithSlug, getPostAndMorePosts } from '@/lib/cosmic'
import PostTitle from '@/components/PostTitle'
import Head from 'next/head'
import markdownToHtml from '@/lib/markdownToHtml'
import AlertPreview from '@/components/AlertPreview'
import PageNotFound from '../404'
import Loader from '@/components/Loader'

const Post = ({ post }) => {
  const router = useRouter()
  if (!router.isFallback && !post?.slug) {
        // Checking if the page exists and redirecting to a 404 page if it doesn't.
    return <PageNotFound />
  }
  return (
    <>
      {router.isFallback ? (
        <PostTitle>
          <div className="flex justify-center items-center">
                        // If you have a custom loader, you can use it here, if not just fill in the text "Loading..."
            <Loader />
          </div>
        </PostTitle>
      ) : (
        <>
                    <article className="border-b border-back-subtle py-8 mb-8">
            {post.status === 'draft' && <AlertPreview />}
            <PostHeader post={post} />
            <PostBody content={post.content} />
          </article>
        </>
      )}
    </>
  )
}
export default Post
// Here is where we get all of the posts from Cosmic, and pass the data into the { post } prop.
export async function getStaticProps({ params, preview = null }) {
  const data = await getPostAndMorePosts(params.slug, preview)
// We're calling that function we wrote earlier in /lib/markdownToHtml.js to convert our Markdown to HTML and send it to our <PostBody> component.
  const content = await markdownToHtml(data.post?.metadata?.content || '')

  return {
    props: {
      preview,
      post: {
        ...data.post,
        content,
      },
      morePosts: data.morePosts || [],
    },
  }
}

export async function getStaticPaths() {
  const allPosts = (await getAllPostsWithSlug()) || []
  return {
    paths: allPosts.map(post => `/posts/${post.slug}`),
    fallback: true,
  }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Теперь, когда мы внедрили код в страницу [slug].jsx, мы можем нажать на любой пост из списка постов и просмотреть содержимое нашего поста.

Создание списка работ и страницы работ

Теперь, когда у нас есть структура для страницы поста и индексной страницы поста, мы можем повторить ее для нашей рабочей страницы. Мы можем создать папку «works» в папке «pages», а затем index.jsx и [slug].jsx.

Скопируйте код из index.jsx и [slug].jsx в pages/posts, и просто измените экземпляры «post(s)» на «work(s)».

export async function getStaticProps({ preview }) {
  const allWorks = (await getAllPosts(preview, 'works')) || []
  const allWorkCategories = (await getAllCategories('work-categories')) || []
  return {
    props: { allWorks, allWorkCategories },
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Использование режима предварительного просмотра

С помощью Next.js и Cosmic мы можем просматривать черновики наших сообщений до их публикации. В Cosmic создайте пост и, заполнив метаполя, выберите «Сохранить черновик», а не «Опубликовать».

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

  1. Возьмите COSMIC_PREVIEW_SECRET, который вы создали ранее. Затем нажмите на значок настроек на вашем объекте в Cosmic.

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

    Обратите внимание, что наша ссылка установлена на локальный хост, и режим предварительного просмотра будет работать только тогда, когда мы запустим наш локальный сервер разработки. Когда ваше приложение будет развернуто, вы можете заменить «http://localhost:3000» на имя вашего домена.

  1. Давайте вернемся к нашему файлу cosmic.js и создадим функцию, которая получает пост предварительного просмотра из Cosmic.
// nextjs-developer-portfolio/src/lib/cosmic.js

export async function getPreviewPostBySlug(slug) {
  const params = {
    query: { slug },
    status: 'any',
    props: 'slug',
  }

  try {
    const data = await bucket.getObjects(params)
    return data.objects[0]
  } catch (error) {
    // Throw error if a slug doesn't exist
    if (is404(error)) return
    throw error
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима
  1. Теперь давайте создадим два маршрута API в нашем проекте — один для самого предварительного просмотра, а другой для выхода из предварительного просмотра. К счастью, Next.js работает с маршрутами API из коробки.
// nextjs-developer-portfolio/src/pages/api/preview.js

import { getPreviewPostBySlug } from '@/lib/cosmic'

export default async function preview(req, res) {
  // Check the secret and next parameters
  // This secret should only be known to this API route and the CMS
  if (
    req.query.secret !== process.env.COSMIC_PREVIEW_SECRET ||
    !req.query.slug
  ) {
    return res.status(401).json({ message: 'Invalid token' })
  }

  // Fetch the headless CMS to check if the provided `slug` exists
  const post = await getPreviewPostBySlug(req.query.slug)

  // If the slug doesn't exist prevent preview mode from being enabled
  if (!post) {
    return res.status(401).json({ message: 'Invalid slug' })
  }

  // Enable Preview Mode by setting the cookies
  res.setPreviewData({})

  // Redirect to the path from the fetched post
  // We don't redirect to req.query.slug as that might lead to open redirect vulnerabilities
  res.writeHead(307, { Location: `/posts/${post.slug}` })
  res.end()
}
Вход в полноэкранный режим Выход из полноэкранного режима
// nextjs-developer-portfolio/src/pages/api/exit-preview.js

export default async function exit(_, res) {
  // Exit the current user from "Preview Mode". This function accepts no args.
  res.clearPreviewData()

  // Redirect the user back to the index page.
  res.writeHead(307, { Location: '/' })
  res.end()
}
Вход в полноэкранный режим Выход из полноэкранного режима
  1. Теперь мы можем вернуться к нашему посту в Cosmic, выбрать кнопку «предварительный просмотр», и наше приложение откроет предварительный просмотр нашего поста.

  1. Прежде чем мы закончим с режимом предварительного просмотра, нам нужно создать компонент, который предупредит нас, если мы находимся в режиме предварительного просмотра, со ссылкой для выхода из режима предварительного просмотра. Эта ссылка приведет нас к API-маршруту «exit-preview.js», который мы создали выше.
// nextjs-developer-portfolio/src/components/AlertPreview.jsx

import Link from 'next/link'

const AlertPreview = () => {
  return (
    <div className="fixed z-20 top-12 left-0 text-fore-subtle bg-back-subtle px-8">
      <div className="py-2 text-center text-sm">
        <>
          This page is a draft.{' '}
          <Link href="/api/exit-preview">
            <a className="underline hover:text-cyan duration-200 transition-colors cursor-pointer">
              Click here
            </a>
          </Link>{' '}
          to exit preview mode.
        </>
      </div>
    </div>
  )
}
export default AlertPreview
Вход в полноэкранный режим Выход из полноэкранного режима

  1. Теперь, когда мы сделали наш баннер, все, что нам нужно сделать, это импортировать его в наши страницы [slug].jsx. По умолчанию наш объект Cosmic поставляется с парой ключ-значение «статус». Если наш пост не опубликован, он имеет статус «draft».
// nextjs-developer-portfolio/src/pages/{posts&works}/[slug].jsx

import AlertPreview from '@/components/AlertPreview'

    ...<article className="border-b border-back-subtle py-8 mb-8">
          {post.status === 'draft' && <AlertPreview />}
          <PostHeader post={post} />
          <PostBody content={post.content} />
       </article>...
Вход в полноэкранный режим Выход из полноэкранного режима

Развертывание в Vercel

Чтобы развернуть свой проект на Vercel, нажмите здесь. Эта ссылка автоматически клонирует шаблон в новый репозиторий, соберет и развернет ваше новое приложение (как здорово!). Все, что вам нужно сделать, это указать переменные окружения, указанные ранее.

Заключение

Теперь у вас есть полнофункциональное портфолио разработчика, которое вы можете использовать для демонстрации своих проектов и обмена записями в блоге с техническим сообществом. Надеюсь, вам понравилось это руководство, и если у вас есть отзывы или вопросы, присоединяйтесь к нам на Slack-канале Cosmic.

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