Π‘Π»ΠΎΠ³

πŸ‘­ Π‘ΠΎΠ·Π΄Π°Ρ‘ΠΌ 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

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

Π Π°Π·Π΄Π΅Π» Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ Π½Π° gatographql.com
Π Π°Π·Π΄Π΅Π» Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ Π½Π° gatographql.com
Π Π°Π·Π΄Π΅Π» Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ Π½Π° gatoplugins.com
Π Π°Π·Π΄Π΅Π» Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ Π½Π° gatoplugins.com

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

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

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

(ΠšΡΡ‚Π°Ρ‚ΠΈ, Π»ΠΎΠ³ΠΎΡ‚ΠΈΠΏΡ‹ Ρ‚ΠΎΠΆΠ΅ ΠΏΠΎΡ‡Ρ‚ΠΈ ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²Ρ‹Π΅! 😜)

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

Π”Π°, эта ΡΡ‚Π°Ρ‚ΡŒΡ Ρ‚ΠΎΠΆΠ΅ Π΅ΡΡ‚ΡŒ Π½Π° ΠΎΠ±ΠΎΠΈΡ… сайтах! πŸ˜‚

Π§ΠΈΡ‚Π°ΠΉΡ‚Π΅ Π½Π° gatographql.com: Building 2 Nextjs websites at the price of 1, by hacking the dark/light 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": "2.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 = "Our 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>
  )
}

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

  • Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠ° страницы (Π½Π°Π·Π²Π°Π½ΠΈΠ΅/описаниС)
  • ЗаписСй Π±Π»ΠΎΠ³Π°
  • Π‘Π°Π½Π½Π΅Ρ€Π° ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ

ΠŸΠΎΡΠΊΠΎΠ»ΡŒΠΊΡƒ Π΄Π²Π° сайта ΠΌΠΎΠ³ΡƒΡ‚ ΠΏΡ€ΠΎΠ²ΠΎΠ΄ΠΈΡ‚ΡŒ собствСнныС ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ нСзависимо Π΄Ρ€ΡƒΠ³ ΠΎΡ‚ Π΄Ρ€ΡƒΠ³Π°, ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‡Π° 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>
        <li>
          <Link href='/roadmap'>Roadmap</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-700):

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-700`}>
        {children}
      </body>
    </html>
  )
}

Π’ΠΎΡ‚ ΠΈ всё

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

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


Π£Π·Π½Π°ΠΉΡ‚Π΅, Ρ‡Ρ‚ΠΎ Π±ΡƒΠ΄Π΅Ρ‚ дальшС

ΠŸΠΎΠ΄ΠΏΠΈΡˆΠΈΡ‚Π΅ΡΡŒ Π½Π° Π½Π°ΡˆΡƒ рассылку: ΡƒΠ·Π½Π°Π²Π°ΠΉΡ‚Π΅, ΠΊΠΎΠ³Π΄Π° ΠΌΡ‹ выпускаСм Π½ΠΎΠ²ΡƒΡŽ Π²Π΅Ρ€ΡΠΈΡŽ, запускаСм Π½ΠΎΠ²Ρ‹ΠΉ ΠΏΠ»Π°Π³ΠΈΠ½ ΠΈΠ»ΠΈ Ρ…ΠΎΡ‚ΠΈΠΌ ΠΏΠΎΠ΄Π΅Π»ΠΈΡ‚ΡŒΡΡ с Π²Π°ΠΌΠΈ новостями.