Π‘Π»ΠΎΠ³

πŸ‘­ Π‘ΠΎΠ·Π΄Π°Ρ‘ΠΌ 2 сайта Π½Π° Next.js ΠΏΠΎ Ρ†Π΅Π½Π΅ ΠΎΠ΄Π½ΠΎΠ³ΠΎ, ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡ Ρ‚Ρ‘ΠΌΠ½Ρ‹ΠΉ/свСтлый Ρ€Π΅ΠΆΠΈΠΌ

Leonardo Losoviz
Автор: Leonardo Losoviz Β·

НСдавно ΠΊΠΎΠΌΠ°Π½Π΄Π° Gato GraphQL запустила Gato Plugins β€” сайт-ΠΏΠΎΠ±Ρ€Π°Ρ‚ΠΈΠΌ Gato GraphQL.

Π’Ρ‹ Π·Π°ΠΌΠ΅Ρ‚ΠΈΡ‚Π΅, Ρ‡Ρ‚ΠΎ ΠΎΠ±Π° сайта β€” это ΠΎΠ΄ΠΈΠ½ ΠΈ Ρ‚ΠΎΡ‚ ΠΆΠ΅ сайт! ЕдинствСнноС ΠΎΡ‚Π»ΠΈΡ‡ΠΈΠ΅ ΠΌΠ΅ΠΆΠ΄Ρƒ Π½ΠΈΠΌΠΈ β€” цвСтовая схСма: Gato GraphQL ΠΎΡ„ΠΎΡ€ΠΌΠ»Π΅Π½ Π² Ρ‚Ρ‘ΠΌΠ½ΠΎΠΉ Ρ‚Π΅ΠΌΠ΅, Π° Gato Plugins β€” Π² свСтлой.

Π Π°Π·Π΄Π΅Π» Π±Π»ΠΎΠ³Π° Π½Π° ΠΎΠ±ΠΎΠΈΡ… сайтах Π°Π±ΡΠΎΠ»ΡŽΡ‚Π½ΠΎ ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²:

Π Π°Π·Π΄Π΅Π» Π±Π»ΠΎΠ³Π° Π½Π° gatographql.com
Π Π°Π·Π΄Π΅Π» Π±Π»ΠΎΠ³Π° Π½Π° gatographql.com
Π Π°Π·Π΄Π΅Π» Π±Π»ΠΎΠ³Π° Π½Π° gatoplugins.com
Π Π°Π·Π΄Π΅Π» Π±Π»ΠΎΠ³Π° Π½Π° gatoplugins.com

Π Π°Π·Π΄Π΅Π» Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ Ρ‚ΠΎΠΆΠ΅ ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²:

Π Π°Π·Π΄Π΅Π» docs Π½Π° gatographql.com
Π Π°Π·Π΄Π΅Π» docs Π½Π° gatographql.com
Π Π°Π·Π΄Π΅Π» docs Π½Π° gatoplugins.com
Π Π°Π·Π΄Π΅Π» docs Π½Π° gatoplugins.com

Иногда Ρ€Π°Π·Π΄Π΅Π»Ρ‹ ΠΎΡ‚Π»ΠΈΡ‡Π°ΡŽΡ‚ΡΡ, ΠΎΠ΄Π½Π°ΠΊΠΎ общая основа ΠΎΠ΄Π½Π° ΠΈ Ρ‚Π° ΠΆΠ΅.

НапримСр, Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΡ Gato GraphQL ΠΈ ΠΏΠ»Π°Π³ΠΈΠ½Ρ‹ Gato Plugins ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡŽΡ‚ ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²Ρ‹ΠΉ ΠΌΠ°ΠΊΠ΅Ρ‚:

Π Π°Π·Π΄Π΅Π» Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠΉ Π½Π° gatographql.com
Π Π°Π·Π΄Π΅Π» Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠΉ Π½Π° gatographql.com
Π Π°Π·Π΄Π΅Π» ΠΏΠ»Π°Π³ΠΈΠ½ΠΎΠ² Π½Π° gatoplugins.com
Π Π°Π·Π΄Π΅Π» ΠΏΠ»Π°Π³ΠΈΠ½ΠΎΠ² Π½Π° gatoplugins.com

(ΠšΡΡ‚Π°Ρ‚ΠΈ, Π»ΠΎΠ³ΠΎΡ‚ΠΈΠΏΡ‹ Ρ‚ΠΎΠΆΠ΅ практичСски ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²Ρ‹Π΅! 😜)

Π›ΠΎΠ³ΠΎΡ‚ΠΈΠΏ Π½Π° gatographql.com
Π›ΠΎΠ³ΠΎΡ‚ΠΈΠΏ Π½Π° gatographql.com
Π›ΠΎΠ³ΠΎΡ‚ΠΈΠΏ Π½Π° gatoplugins.com
Π›ΠΎΠ³ΠΎΡ‚ΠΈΠΏ Π½Π° gatoplugins.com

И Π΄Π°, эта ΡΡ‚Π°Ρ‚ΡŒΡ Ρ‚ΠΎΠΆΠ΅ ΠΎΠΏΡƒΠ±Π»ΠΈΠΊΠΎΠ²Π°Π½Π° Π½Π° ΠΎΠ±ΠΎΠΈΡ… сайтах! πŸ˜‚

Π§ΠΈΡ‚Π°ΠΉΡ‚Π΅ Π½Π° gatoplugins.com: Building 2 Nextjs websites at the price of 1, by hacking the light/dark mode.

Однако ΠΌΠ΅ΠΆΠ΄Ρƒ публикациями Π½Π° Π΄Π²ΡƒΡ… сайтах Π΅ΡΡ‚ΡŒ Ρ€ΠΎΠ²Π½ΠΎ 7 ΠΎΡ‚Π»ΠΈΡ‡ΠΈΠΉ. Π‘ΠΌΠΎΠΆΠ΅Ρ‚Π΅ Π½Π°ΠΉΡ‚ΠΈ ΠΈΡ… всС? Если Π΄Π°, я Π΄Π°ΠΌ Π²Π°ΠΌ ΠΊΡƒΠΏΠΎΠ½ со скидкой Π½Π° Gato GraphQL πŸ™

ΠŸΠΎΡ‡Π΅ΠΌΡƒ ΠΌΡ‹ использовали свСтлый/Ρ‚Ρ‘ΠΌΠ½Ρ‹ΠΉ Ρ€Π΅ΠΆΠΈΠΌ для создания 2 сайтов

ΠŸΡ€ΠΈΡ‡ΠΈΠ½ нСсколько:

Π£ мСня Π½Π΅Ρ‚ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ ΠΈ сил ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Ρ‚ΡŒ Π΄Π²Π΅ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹Π΅ ΠΊΠΎΠ΄ΠΎΠ²Ρ‹Π΅ Π±Π°Π·Ρ‹. МнС Π½ΡƒΠΆΠ½ΠΎ Π΄Π΅Ρ€ΠΆΠ°Ρ‚ΡŒ всё простым.

ΠšΠ°ΠΆΠ΄Ρ‹ΠΉ час, ΠΏΠΎΡ‚Ρ€Π°Ρ‡Π΅Π½Π½Ρ‹ΠΉ Π½Π° сайт, β€” это час, Π½Π΅ ΠΏΠΎΡ‚Ρ€Π°Ρ‡Π΅Π½Π½Ρ‹ΠΉ Π½ΠΈ Π½Π° ΠΎΠ΄ΠΈΠ½ ΠΈΠ· ΠΌΠΎΠΈΡ… ΠΏΡ€ΠΎΠ΄ΡƒΠΊΡ‚ΠΎΠ².

Π― Ρ…ΠΎΡ‡Ρƒ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΎΠ½ΠΈ выглядСли ΠΏΠΎΡ…ΠΎΠΆΠ΅, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΠΈ ΠΌΠΎΠ³Π»ΠΈ ΡƒΠ·Π½Π°Ρ‚ΡŒ ΠΈΡ… ΠΊΠ°ΠΊ Ρ‡Π°ΡΡ‚ΡŒ ΠΎΠ΄Π½ΠΎΠ³ΠΎ сСмСйства.

Π― Π½Π΅ Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ€. Достигнув Ρ‚Π°ΠΊΠΎΠ³ΠΎ внСшнСго Π²ΠΈΠ΄Π° ΠΈ стиля, я Π±Ρ‹Π» Π΄ΠΎΠ²ΠΎΠ»Π΅Π½ ΠΈ Π½Π΅ Ρ…ΠΎΡ‚Π΅Π» Π½Π°Ρ‡ΠΈΠ½Π°Ρ‚ΡŒ с нуля.

Π˜Π½Ρ‹ΠΌΠΈ словами: ΠΏΠΎΡ‚ΠΎΠΌΡƒ Ρ‡Ρ‚ΠΎ это Π΄Ρ‘ΡˆΠ΅Π²ΠΎ ΠΈ просто. Π­Ρ‚ΠΎ сэкономило ΠΌΠ½Π΅ массу Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ ΠΈ сил, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ я смог Π½Π°ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ Π½Π° собствСнный ΠΏΡ€ΠΎΠ΄ΡƒΠΊΡ‚.

Из нСдостатков: 2 сайта Π½Π΅ ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°ΡŽΡ‚ ΠΏΠ΅Ρ€Π΅ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ Ρ‚Ρ‘ΠΌΠ½ΠΎΠ³ΠΎ/свСтлого Ρ€Π΅ΠΆΠΈΠΌΠ°, поэтому ΠΈΡ… ΡΡ‚ΠΈΠ»ΡŒ фиксирован, Π½ΠΎ с этим я Π³ΠΎΡ‚ΠΎΠ² ΠΌΠΈΡ€ΠΈΡ‚ΡŒΡΡ.


Π˜Ρ‚Π°ΠΊ! Засучим Ρ€ΡƒΠΊΠ°Π²Π° ΠΈ посмотрим, ΠΊΠ°ΠΊ это Π±Ρ‹Π»ΠΎ сдСлано.

Π‘Ρ‚Π΅ΠΊ: ΠŸΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ основано Π½Π° Next.js ΠΈ Tailwind CSS для стилизации.

Оно Π±Ρ‹Π»ΠΎ создано Π½Π° основС Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ… шаблонов ΠΎΡ‚ Cruip, Π°Π΄Π°ΠΏΡ‚ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Ρ… ΠΏΠΎΠ΄ наши Π½ΡƒΠΆΠ΄Ρ‹. (Π­Ρ‚ΠΈ ΡˆΠ°Π±Π»ΠΎΠ½Ρ‹ просто прСкрасны!)

ΠšΠΎΠ½Ρ‚Π΅Π½Ρ‚ управляСтся Ρ‡Π΅Ρ€Π΅Π· Contentlayer.

Выносим ΠΎΠ±Ρ‰ΠΈΠΉ ΠΊΠΎΠ΄ Π² ΠΎΠ±Ρ‰ΠΈΠΉ ΠΏΠ°ΠΊΠ΅Ρ‚ ΠΈ Ρ€Π°Π·ΠΌΠ΅Ρ‰Π°Π΅ΠΌ всё Π² ΠΌΠΎΠ½ΠΎΡ€Π΅ΠΏΠΎ

ΠŸΠΎΡΠΊΠΎΠ»ΡŒΠΊΡƒ кодовая Π±Π°Π·Π° для ΠΎΠ±ΠΎΠΈΡ… сайтов ΠΎΠ΄Π½Π°, Π»ΠΎΠ³ΠΈΡ‡Π½ΠΎ Ρ€Π°Π·ΠΌΠ΅ΡΡ‚ΠΈΡ‚ΡŒ ΠΈΡ… всС вмСстС Π² ΠΌΠΎΠ½ΠΎΡ€Π΅ΠΏΠΎ.

Π˜Π·Π½Π°Ρ‡Π°Π»ΡŒΠ½ΠΎ Π² ΠΌΠΎΡ‘ΠΌ Ρ€Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΈ Π±Ρ‹Π» СдинствСнный ΠΏΡ€ΠΎΠ΅ΠΊΡ‚:

  • gatographql.com

Он Π±Ρ‹Π» рСструктурирован ΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΠΈΠΌ ΠΎΠ±Ρ€Π°Π·ΠΎΠΌ:

  • apps/gatographql.com: Π‘Π°ΠΉΡ‚ Gato GraphQL
  • apps/gatoplugins.com: Π‘Π°ΠΉΡ‚ Gato Plugins
  • packages/shared/gatoapp: ΠžΠ±Ρ‰ΠΈΠΉ ΠΊΠΎΠ΄ для ΠΎΠ±ΠΎΠΈΡ… сайтов

Π’ΠΎΡ‚ ΠΊΠ°ΠΊ выглядит ΠΌΠΎΡ‘ Ρ€Π°Π±ΠΎΡ‡Π΅Π΅ пространство Π² VSCode:

Π‘Ρ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€Π° ΠΌΠΎΠ΅Π³ΠΎ ΠΌΠΎΠ½ΠΎΡ€Π΅ΠΏΠΎ
Π‘Ρ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€Π° ΠΌΠΎΠ΅Π³ΠΎ ΠΌΠΎΠ½ΠΎΡ€Π΅ΠΏΠΎ

Π― Π½Π΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡŽ Π½ΠΈΡ‡Π΅Π³ΠΎ слоТного для ΠΌΠΎΠ½ΠΎΡ€Π΅ΠΏΠΎ β€” простой workspaces прСкрасно справляСтся.

Мой package.json Π² ΠΊΠΎΡ€Π½Π΅ ΠΌΠΎΠ½ΠΎΡ€Π΅ΠΏΠΎ Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ выглядит Ρ‚Π°ΠΊ:

{
  "name": "gatowebsites",
  "version": "3.0.0",
  "private": true,
  "workspaces": [
    "apps/*",
    "packages/shared/*"
  ]
}

ΠšΡ€ΠΎΠΌΠ΅ Ρ‚ΠΎΠ³ΠΎ, я Π΄ΠΎΠ±Π°Π²ΠΈΠ» скрипты Π² package.json для запуска/сборки/дСплоя ΠΎΠ±ΠΎΠΈΡ… ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ΠΎΠ² (Π²ΠΊΠ»ΡŽΡ‡Π°Ρ Π΄Π΅ΠΏΠ»ΠΎΠΉ Π½Π° Netlify, Π³Π΄Π΅ ΠΎΠ±Π° ΠΎΠ½ΠΈ Ρ€Π°Π·ΠΌΠ΅Ρ‰Π΅Π½Ρ‹):

{
  "scripts": {
    "dev-gatographql": "npm run dev --workspace=apps/gatographql",
    "build-gatographql": "npm run build --workspace=apps/gatographql",
    "deploy-gatographql": "npm run deploy-staging-gatographql",
    "deploy-dev-gatographql": "netlify dev --filter gatographql",
    "deploy-staging-gatographql": "netlify deploy --build --context deploy-preview --filter gatographql",
    "deploy-prod-gatographql": "netlify deploy --build --prod --context production --filter gatographql",
 
    "dev-gatoplugins": "npm run dev --workspace=apps/gatoplugins",
    "build-gatoplugins": "npm run build --workspace=apps/gatoplugins",
    "deploy-gatoplugins": "npm run deploy-staging-gatoplugins",
    "deploy-dev-gatoplugins": "netlify dev --filter gatoplugins",
    "deploy-staging-gatoplugins": "netlify deploy --build --context deploy-preview --filter gatoplugins",
    "deploy-prod-gatoplugins": "netlify deploy --build --prod --context production --filter gatoplugins"
  }
}

ΠŸΡ€Π΅ΠΎΠ±Ρ€Π°Π·ΡƒΠ΅ΠΌ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹ для получСния пропсов с ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΈΠΌΠΈ Π΄Π°Π½Π½Ρ‹ΠΌΠΈ

По возмоТности ΠΌΡ‹ пСрСносим ΠΊΠΎΠ΄ ΠΈΠ· ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ ΠΈΠ· сайтов Π² ΠΎΠ±Ρ‰ΠΈΠΉ ΠΏΠ°ΠΊΠ΅Ρ‚, Π° Π·Π°Ρ‚Π΅ΠΌ настраиваСм ΠΏΠΎΠ²Π΅Π΄Π΅Π½ΠΈΠ΅ Ρ‡Π΅Ρ€Π΅Π· пропсы.

НапримСр, ΠΎΠ±Ρ‰ΠΈΠΉ ΠΏΠ°ΠΊΠ΅Ρ‚ gatoapp содСрТит ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ BlogSection (для отобраТСния страницы /blog Π½Π° ΠΎΠ±ΠΎΠΈΡ… сайтах):

import PopularPosts from 'gatoapp/components/blog/popular-posts'
import PageHeader from 'gatoapp/components/page-header'
import { BlogPostProps } from 'gatoapp/types/list-types'
import BlogSectionPostList from './blog-section-post-list'
import { useEffect, useState, Suspense } from "react";
 
export default function BlogSection({
  blogPosts,
  title = "Blog",
  description,
  campaignBanner,
}: {
  blogPosts: BlogPostProps[],
  title?: string,
  description: string,
  campaignBanner?: React.ReactNode
}) {
  const sidebar = (
    <aside className="hidden sm:block relative mt-12 md:mt-0 md:w-64 md:ml-12 lg:ml-20 md:shrink-0">
      <PopularPosts
        blogPosts={blogPosts}
      />
    </aside>
  )
 
  return (
    <div className="max-w-6xl mx-auto px-4 sm:px-6">
      <div className="pt-32 pb-12 md:pt-40 md:pb-20">
 
        {campaignBanner}
 
        {/* Page header */}
        <PageHeader
          title={title}
          description={description}
        />
 
        {/* Main content */}
        <BlogSectionPostList
          blogPosts={blogPosts}
          sidebar={sidebar}
        />
 
      </div>
    </div>
  )
}

Π’Π΅ΡΡŒ ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚ ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ², Π·Π° ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ΠΌ:

  • Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠ° страницы (title/description)
  • ΠŸΠΎΡΡ‚ΠΎΠ² Π±Π»ΠΎΠ³Π°
  • Π‘Π°Π½Π½Π΅Ρ€Π° ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ

ΠŸΠΎΡΠΊΠΎΠ»ΡŒΠΊΡƒ Π΄Π²Π° сайта ΠΌΠΎΠ³ΡƒΡ‚ ΠΏΡ€ΠΎΠ²ΠΎΠ΄ΠΈΡ‚ΡŒ собствСнныС ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ нСзависимо Π΄Ρ€ΡƒΠ³ ΠΎΡ‚ Π΄Ρ€ΡƒΠ³Π°, ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‡Π° campaignBanner ΠΊΠ°ΠΊ React.ReactNode Π½Π΅ ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡ΠΈΠ²Π°Π΅Ρ‚ настройку ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΉ.

НапримСр, ΠΏΠΎΠΊΠ° я ΠΏΡƒΠ±Π»ΠΈΠΊΡƒΡŽ эту ΡΡ‚Π°Ρ‚ΡŒΡŽ, я ΠΏΡ€ΠΎΠ²ΠΎΠΆΡƒ кампанию Π² Gato GraphQL, Π½ΠΎ Π½Π΅ Π² Gato Plugins:

Π‘Π°Π½Π½Π΅Ρ€ ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ Π½Π° gatographql.com
Π‘Π°Π½Π½Π΅Ρ€ ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ Π½Π° gatographql.com

Для внСдрСния постов Π±Π»ΠΎΠ³Π° трСбуСтся Π½Π΅ΠΌΠ½ΠΎΠ³ΠΎ большС Π»ΠΎΠ³ΠΈΠΊΠΈ.

Π’Π½Π΅Π΄Ρ€Π΅Π½ΠΈΠ΅ постов Π±Π»ΠΎΠ³Π°

Π”Π°Π½Π½Ρ‹Π΅ для постов Π±Π»ΠΎΠ³Π° ΠΏΠ΅Ρ€Π΅Π΄Π°ΡŽΡ‚ΡΡ Π² BlogSection Ρ‡Π΅Ρ€Π΅Π· ΠΏΡ€ΠΎΠΏ blogPosts.

ΠŸΠΎΡΠΊΠΎΠ»ΡŒΠΊΡƒ я ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡŽ Contentlayer, ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ сайт Π±ΡƒΠ΄Π΅Ρ‚ ΠΈΠΌΠ΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ» contentlayer.config.js Π² ΠΊΠΎΡ€Π½Π΅, ΠΎΠΏΡ€Π΅Π΄Π΅Π»ΡΡŽΡ‰ΠΈΠΉ Ρ‚ΠΈΠΏΡ‹ сайта.

Π­Ρ‚ΠΎΡ‚ Ρ„Π°ΠΉΠ» ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ нСльзя пСрСнСсти Π² ΠΎΠ±Ρ‰ΠΈΠΉ gatoapp. ΠŸΠΎΡΡ‚ΠΎΠΌΡƒ ΠΌΡ‹ создаём экспортный ΠΌΠΎΠ΄ΡƒΠ»ΡŒ для прСдоставлСния ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ ΠΎΠ±Ρ‰ΠΈΡ… Ρ‚ΠΈΠΏΠΎΠ², Π° Π·Π°Ρ‚Π΅ΠΌ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠ΅ΠΌ ΠΈΡ… Π² contentlayer.config.js ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ сайта, дСлая Π»ΠΎΠ³ΠΈΠΊΡƒ DRY.

gatoapp ΠΈΠΌΠ΅Π΅Ρ‚ экспортный ΠΌΠΎΠ΄ΡƒΠ»ΡŒ contentlayer.config.js, ΠΏΡ€Π΅Π΄ΠΎΡΡ‚Π°Π²Π»ΡΡŽΡ‰ΠΈΠΉ ΠΎΠ±Ρ‰ΠΈΠΉ Ρ‚ΠΈΠΏ BlogPost:

import { defineDocumentType } from 'contentlayer2/source-files'
 
const BlogPost = defineDocumentType(() => ({
  name: 'BlogPost',
  filePathPattern: `blog/**/*.mdx`,
  contentType: 'mdx',
  fields: {
    title: {
      type: 'string',
      required: true
    },
    publishedAt: {
      type: 'date',
      required: true
    },
    description: {
      type: 'string',
      required: true,
    },
    image: {
      type: 'string',
    },
  },
  computedFields: {
    slug: {
      type: 'string',
      resolve: (doc) => doc._raw.flattenedPath.replace(new RegExp('^blog/?'), ''),
    },
    urlPath: {
      type: 'string',
      resolve: (doc) => `/blog/${doc._raw.flattenedPath.replace(new RegExp('^blog/?'), '')}`,
    },
  },
}))
 
module.exports = {
  types: {
    BlogPost: BlogPost,
  },
}

Π€Π°ΠΉΠ» contentlayer.config.js Π² apps/gatographql.com ΠΈ apps/gatoplugins.com ΠΌΠΎΠΆΠ΅Ρ‚ Π·Π°Ρ‚Π΅ΠΌ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ этот Ρ‚ΠΈΠΏ:

import { makeSource } from 'contentlayer2/source-files'
import ContentLayerConfig from '../../packages/shared/gatoapp/contentlayer.config.js'
 
const BlogPost = ContentLayerConfig.types.BlogPost
 
export default makeSource({
  documentTypes: [BlogPost],
})

ΠžΠ±Ρ‹Ρ‡Π½ΠΎ для обращСния ΠΊ Ρ‚ΠΈΠΏΡƒ BlogPost Π² нашСм ΠΊΠΎΠ΄Π΅ ΠΌΡ‹ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠ΅ΠΌ Π΅Π³ΠΎ Ρ‚Π°ΠΊ:

import { BlogPost } from '@/.contentlayer/generated'

Однако Ρ‚ΠΈΠΏ BlogPost находится ΠΏΠΎΠ΄ сайтом, Π° Π½Π΅ ΠΏΠΎΠ΄ ΠΎΠ±Ρ‰ΠΈΠΌ ΠΏΠ°ΠΊΠ΅Ρ‚ΠΎΠΌ, поэтому ΠΎΠ±Ρ‰ΠΈΠΉ ΠΊΠΎΠ΄ Π½Π΅ ΠΌΠΎΠΆΠ΅Ρ‚ Π½Π°ΠΏΡ€ΡΠΌΡƒΡŽ ΡΡΡ‹Π»Π°Ρ‚ΡŒΡΡ Π½Π° этот Ρ‚ΠΈΠΏ.

ΠœΡ‹ Ρ€Π΅ΡˆΠ°Π΅ΠΌ это с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ Ρ…Π°ΠΊΠ°: ΠΊΠΎΠΏΠΈΡ€ΡƒΠ΅ΠΌ ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½ΠΈΠ΅ этого Ρ‚ΠΈΠΏΠ° ΠΈΠ· скомпилированного Ρ„Π°ΠΉΠ»Π° Contentlayer (ΠΈΠ· apps/gatographql/.contentlayer/generated/types.d.ts) ΠΈ вставляСм Π΅Π³ΠΎ Π² Π½ΠΎΠ²Ρ‹ΠΉ Ρ„Π°ΠΉΠ» types.tsx Π² ΠΎΠ±Ρ‰Π΅ΠΌ ΠΏΠ°ΠΊΠ΅Ρ‚Π΅:

import type { MDX, IsoDateTimeString } from 'contentlayer2/core'
 
export type BlogPost = {
  // _id: string // not needed
  // _raw: Local.RawDocumentData // not needed
  type: 'BlogPost'
  title: string
  publishedAt: IsoDateTimeString
  description: string
  image?: string | undefined
  body: MDX
  slug: string,
  urlPath: string,
}

Π—Π°Ρ‚Π΅ΠΌ ΠΌΡ‹ ссылаСмся Π½Π° этот ΠΎΠ±Ρ‰ΠΈΠΉ Ρ‚ΠΈΠΏ Π² ΠΎΠ±Ρ‰Π΅ΠΌ ΠΊΠΎΠ΄Π΅:

import { BlogPost } from 'gatoapp/types'

ΠŸΠΎΡΠΊΠΎΠ»ΡŒΠΊΡƒ свойства Ρ‚ΠΈΠΏΠΎΠ² BlogPost Π½Π° сайтС ΠΈ Π² ΠΎΠ±Ρ‰Π΅ΠΌ ΠΏΠ°ΠΊΠ΅Ρ‚Π΅ ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²Ρ‹, ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ ΠΏΠ΅Ρ€Π΅Π΄Π°Π²Π°Ρ‚ΡŒ ΠΏΠ΅Ρ€Π²Ρ‹ΠΉ Π² ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚, ΠΎΠΆΠΈΠ΄Π°ΡŽΡ‰ΠΈΠΉ Π²Ρ‚ΠΎΡ€ΠΎΠΉ.

Π‘ΠΎΠ·Π΄Π°Ρ‘ΠΌ контСкст для внСдрСния Π³Π»ΠΎΠ±Π°Π»ΡŒΠ½Ρ‹Ρ… пропсов

ΠšΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹ Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΠΎΠ½Π½ΠΎΠ³ΠΎ мСню Π±ΡƒΠ΄ΡƒΡ‚ ΠΎΡ‚ΠΎΠ±Ρ€Π°ΠΆΠ°Ρ‚ΡŒΡΡ Π² ΠΎΠ±Ρ‰Π΅ΠΌ ΠΊΠΎΠ΄Π΅, Π½ΠΎ ΠΎΠ½ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΠΏΡ€Π΅Π΄ΠΎΡΡ‚Π°Π²Π»ΡΡ‚ΡŒΡΡ Ρ‡Π΅Ρ€Π΅Π· ΠΊΠΎΠ΄ сайта, ΠΏΠΎΡΠΊΠΎΠ»ΡŒΠΊΡƒ Ρƒ ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ сайта Π±ΡƒΠ΄ΡƒΡ‚ свои мСню.

МСню ΠΏΠΎΡΠ²Π»ΡΡŽΡ‚ΡΡ Π½Π° всСх страницах, ΠΈ ΠΌΡ‹ Π½Π΅ Ρ…ΠΎΡ‚ΠΈΠΌ ΠΏΠ΅Ρ€Π΅Π΄Π°Π²Π°Ρ‚ΡŒ ΠΈΡ… Ρ‡Π΅Ρ€Π΅Π· пропсы снова ΠΈ снова. ΠŸΠΎΡΡ‚ΠΎΠΌΡƒ ΠΌΡ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ контСкст React, ΠΏΠΎΠ·Π²ΠΎΠ»ΡΡŽΡ‰ΠΈΠΉ Π½Π°ΠΌ Π²Π½Π΅Π΄Ρ€ΠΈΡ‚ΡŒ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹ Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΠΎΠ½Π½ΠΎΠ³ΠΎ мСню Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΠ΄ΠΈΠ½ Ρ€Π°Π·.

ΠœΡ‹ создаём контСкст AppComponent Π² ΠΎΠ±Ρ‰Π΅ΠΌ ΠΏΠ°ΠΊΠ΅Ρ‚Π΅:

'use client'
 
import React from 'react'
import { createContext, useContext } from 'react'
import { StaticImageData } from 'next/image'
 
type ContextProps = {
  header: {
    menu: React.ReactNode,
    mobileMenu: React.ReactNode,
  },
}
 
const AppComponentContext = createContext<ContextProps>({
  header: {
    menu: <div></div>,
    mobileMenu: <div></div>,
  },
})
 
export interface AppComponentProviderInterface extends ContextProps {
  children: React.ReactNode,
}
 
export default function AppComponentProvider({
  children,
  header,
}: AppComponentProviderInterface) {  
  return (
    <AppComponentContext.Provider value={{ header }}>
      {children}
    </AppComponentContext.Provider>
  )
}
 
export const useAppComponentProvider = () => useContext(AppComponentContext)

ΠœΡ‹ ссылаСмся Π½Π° Π½Π΅Π³ΠΎ Π² нашСм ΠΎΠ±Ρ‰Π΅ΠΌ ΠΏΠ°ΠΊΠ΅Ρ‚Π΅:

'use client'
 
import Logo from './logo'
import HeaderMobile from './header-mobile'
import { useAppComponentProvider } from 'gatoapp/app/appcomponent-provider'
 
export default function Header() {
  const AppComponent = useAppComponentProvider()
  return (
    <header className="fixed w-full z-50">
      <div className={`absolute inset-0 bg-opacity-70 backdrop-blur -z-10 bg-white border-slate-200 border-b dark:border-b-0 dark:bg-transparent dark:border-slate-800`} aria-hidden="true"/>
      <div className="max-w-6xl mx-auto px-4 sm:px-6">
        <div className="flex items-center justify-between h-16">
 
          {/* Site branding */}
          <div className="flex-1">
            <Logo />
          </div>
 
          <nav className="hidden md:flex md:grow">
            {/* Desktop menu links */}
            {AppComponent.header.menu}
          </nav>
 
          <HeaderMobile />
 
        </div>
      </div>
    </header>
  )
}

И внСдряСм Π΅Π³ΠΎ Ρ‡Π΅Ρ€Π΅Π· ΠΊΠΎΠ΄ сайта Π² apps/gatographql/app/(default)/layout.tsx:

import AppComponentProvider from 'gatoapp/app/appcomponent-provider'
import HeaderMenu from '@/components/menu/header-menu'
import HeaderMobileMenu from '@/components/menu/header-mobile-menu'
import DefaultLayout from 'gatoapp/app/(default)/layout'
 
export default function AppDefaultLayout({
  children,
}: {
  children: React.ReactNode
}) {  
  return (
    <AppComponentProvider
      header={{
        menu: <HeaderMenu />,
        mobileMenu: <HeaderMobileMenu />,
      }}
    >
      <DefaultLayout>
        {children}
      </DefaultLayout>
    </AppComponentProvider>
  )
}

НаконСц, сайт Ρ€Π΅Π°Π»ΠΈΠ·ΡƒΠ΅Ρ‚ собствСнный ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ HeaderMenu:

import Link from 'next/link'
import Dropdown from 'gatoapp/components/utils/dropdown'
 
export default function HeaderMenu() {
  return (
    <ul className="flex grow justify-center flex-wrap items-center">
      <li>
        <Link href="/pricing">Pricing</Link>
      </li>
      <li>
        <Link href='/extensions'>Extensions</Link>
      </li>      
      <Dropdown title="Product">
        <li>
          <Link href='/features'>Features</Link>
        </li>
        <li>
          <Link href='/highlights'>Highlights</Link>
        </li>
        <li>
          <Link href='/demos'>Demos</Link>
        </li>
        <li>
          <Link href='/comparisons'>Comparisons</Link>
        </li>
      </Dropdown>
    </ul>
  )
}

Π‘Ρ‚ΠΈΠ»ΠΈ для свСтлого ΠΈ Ρ‚Ρ‘ΠΌΠ½ΠΎΠ³ΠΎ Ρ€Π΅ΠΆΠΈΠΌΠΎΠ²

Π’ Tailwind ΠΌΡ‹ добавляСм ΠΊ классу прСфикс dark:, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Π΅Π³ΠΎ ΠΏΡ€ΠΈ Π²ΠΊΠ»ΡŽΡ‡Ρ‘Π½Π½ΠΎΠΌ Ρ‚Ρ‘ΠΌΠ½ΠΎΠΌ Ρ€Π΅ΠΆΠΈΠΌΠ΅.

ΠŸΠΎΡΡ‚ΠΎΠΌΡƒ ΠΊΠΎΠ΄ нашСго ΠΎΠ±Ρ‰Π΅Π³ΠΎ ΠΏΠ°ΠΊΠ΅Ρ‚Π° Π΄ΠΎΠ»ΠΆΠ΅Π½ ΡΠΎΠ΄Π΅Ρ€ΠΆΠ°Ρ‚ΡŒ стили ΠΊΠ°ΠΊ для свСтлого, Ρ‚Π°ΠΊ ΠΈ для Ρ‚Ρ‘ΠΌΠ½ΠΎΠ³ΠΎ Π²Π°Ρ€ΠΈΠ°Π½Ρ‚Π°.

НапримСр, ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ PageHeader ΠΎΡ‚ΠΎΠ±Ρ€Π°ΠΆΠ°Π΅Ρ‚ описаниС с Ρ€Π°Π·Π½Ρ‹ΠΌΠΈ Ρ†Π²Π΅Ρ‚Π°ΠΌΠΈ для свСтлого Ρ€Π΅ΠΆΠΈΠΌΠ° (text-gray-600) ΠΈ Ρ‚Ρ‘ΠΌΠ½ΠΎΠ³ΠΎ Ρ€Π΅ΠΆΠΈΠΌΠ° (dark:text-slate-400):

export default function PageHeader({
  title,
  description,
  children,
}: {
  title: string,
  description?: string,
  children?: React.ReactNode,
}) {
  return (
    <div className="max-w-3xl mx-auto text-center">
      <h1 className="h1 pb-4">{title}</h1>
      {description && (
        <div className="max-w-3xl mx-auto">
          <p className="text-gray-600 dark:text-slate-400">{description}</p>
        </div>
      )}
      {children}
    </div>
  )
}

УстанавливаСм свСтлый ΠΈΠ»ΠΈ Ρ‚Ρ‘ΠΌΠ½Ρ‹ΠΉ Ρ€Π΅ΠΆΠΈΠΌ Π½Π° сайтС

gatographql.com ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ Ρ‚Ρ‘ΠΌΠ½Ρ‹ΠΉ Ρ€Π΅ΠΆΠΈΠΌ. Он задаётся Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ΠΌ класса dark ΠΊ <body> Π² Ρ„Π°ΠΉΠ»Π΅ apps/gatographql/app/layout.tsx (плюс классы для стилизации: bg-slate-900 text-slate-100):

import { Inter } from 'next/font/google'
import RootLayoutHeader from 'gatoapp/app/layout-header'
 
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap'
})
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <RootLayoutHeader />
      <body className={`${inter.variable} dark bg-slate-900 text-slate-100`}>
        {children}
      </body>
    </html>
  )
}

gatoplugins.com ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ свСтлый Ρ€Π΅ΠΆΠΈΠΌ. Π­Ρ‚ΠΎ Ρ€Π΅ΠΆΠΈΠΌ ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ, поэтому Π½Π΅Ρ‚ нСобходимости Π΄ΠΎΠ±Π°Π²Π»ΡΡ‚ΡŒ ΠΊΠ°ΠΊΠΎΠΉ-Π»ΠΈΠ±ΠΎ особый класс ΠΊ <body> (Ρ‚ΠΎΠ»ΡŒΠΊΠΎ классы для стилизации: bg-white text-slate-800):

import { Inter } from 'next/font/google'
import RootLayoutHeader from 'gatoapp/app/layout-header'
 
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap'
})
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <RootLayoutHeader />
      <body className={`${inter.variable} bg-white text-slate-800`}>
        {children}
      </body>
    </html>
  )
}

Π’ΠΎΡ‚ ΠΈ всё

Π’Π΅ΠΏΠ΅Ρ€ΡŒ Ρƒ мСня Π΅ΡΡ‚ΡŒ 2 сайта, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ я ΠΏΠΎΠ»ΡƒΡ‡ΠΈΠ» ΠΏΠΎ Ρ†Π΅Π½Π΅ ΠΎΠ΄Π½ΠΎΠ³ΠΎ. И я ΠΎΡ‡Π΅Π½ΡŒ Π΄ΠΎΠ²ΠΎΠ»Π΅Π½ этим.

Π’Π΅ΠΏΠ΅Ρ€ΡŒ Π½Π°ΠΉΠ΄ΠΈΡ‚Π΅ 7 ΠΎΡ‚Π»ΠΈΡ‡ΠΈΠΉ ΠΈ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚Π΅ свой ΠΏΡ€ΠΈΠ·! πŸ˜…


ΠŸΠΎΠ΄ΠΏΠΈΡˆΠΈΡ‚Π΅ΡΡŒ Π½Π° Π½Π°ΡˆΡƒ рассылку

Π‘ΡƒΠ΄ΡŒΡ‚Π΅ Π² курсС всСх ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠΉ Gato GraphQL.