π Π‘ΠΎΠ·Π΄Π°ΡΠΌ 2 ΡΠ°ΠΉΡΠ° Π½Π° Next.js ΠΏΠΎ ΡΠ΅Π½Π΅ ΠΎΠ΄Π½ΠΎΠ³ΠΎ, Π²Π·Π»Π°ΠΌΡΠ²Π°Ρ ΡΠ²Π΅ΡΠ»ΡΠΉ/ΡΡΠΌΠ½ΡΠΉ ΡΠ΅ΠΆΠΈΠΌ
ΠΠ΅Π΄Π°Π²Π½ΠΎ ΠΊΠΎΠΌΠ°Π½Π΄Π° Gato GraphQL Π·Π°ΠΏΡΡΡΠΈΠ»Π° Gato Plugins β Π΄ΠΎΡΠ΅ΡΠ½ΠΈΠΉ ΡΠ°ΠΉΡ Gato GraphQL.
ΠΡ Π·Π°ΠΌΠ΅ΡΠΈΡΠ΅, ΡΡΠΎ ΠΎΠ±Π° ΡΠ°ΠΉΡΠ° ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²ΡΠ΅! ΠΠ΄ΠΈΠ½ΡΡΠ²Π΅Π½Π½ΠΎΠ΅ ΠΎΡΠ»ΠΈΡΠΈΠ΅ ΠΌΠ΅ΠΆΠ΄Ρ Π½ΠΈΠΌΠΈ β ΡΠ²Π΅ΡΠΎΠ²Π°Ρ ΡΡ Π΅ΠΌΠ°: Gato GraphQL ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΠ΅Ρ ΡΡΠΌΠ½ΡΡ ΡΠ΅ΠΌΡ, Π° Gato Plugins β ΡΠ²Π΅ΡΠ»ΡΡ.
Π Π°Π·Π΄Π΅Π» Π±Π»ΠΎΠ³Π° Π½Π° ΠΎΠ±ΠΎΠΈΡ ΡΠ°ΠΉΡΠ°Ρ Π°Π±ΡΠΎΠ»ΡΡΠ½ΠΎ ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²:


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


ΠΠ½ΠΎΠ³Π΄Π° ΡΠ°Π·Π΄Π΅Π»Ρ ΠΎΡΠ»ΠΈΡΠ°ΡΡΡΡ, ΠΎΠ΄Π½Π°ΠΊΠΎ Π±Π°Π·ΠΎΠ²Π°Ρ ΠΎΡΠ½ΠΎΠ²Π° ΠΎΠ΄Π½Π° ΠΈ ΡΠ° ΠΆΠ΅.
ΠΠ°ΠΏΡΠΈΠΌΠ΅Ρ, ΡΠ°ΡΡΠΈΡΠ΅Π½ΠΈΡ Gato GraphQL ΠΈ ΠΏΠ»Π°Π³ΠΈΠ½Ρ Gato Plugins ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΡΡ ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²ΡΠΉ ΠΌΠ°ΠΊΠ΅Ρ:


(ΠΡΡΠ°ΡΠΈ, Π»ΠΎΠ³ΠΎΡΠΈΠΏΡ ΡΠΎΠΆΠ΅ ΠΏΠΎΡΡΠΈ ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²ΡΠ΅! π)


ΠΠ°, ΡΡΠ° ΡΡΠ°ΡΡΡ ΡΠΎΠΆΠ΅ Π΅ΡΡΡ Π½Π° ΠΎΠ±ΠΎΠΈΡ ΡΠ°ΠΉΡΠ°Ρ ! π
Π§ΠΈΡΠ°ΠΉΡΠ΅ Π½Π° 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:

ΠΠ»Ρ Π²Π½Π΅Π΄ΡΠ΅Π½ΠΈΡ Π·Π°ΠΏΠΈΡΠ΅ΠΉ Π±Π»ΠΎΠ³Π° ΡΡΠ΅Π±ΡΠ΅ΡΡΡ Π½Π΅ΠΌΠ½ΠΎΠ³ΠΎ Π±ΠΎΠ»ΡΡΠ΅ Π»ΠΎΠ³ΠΈΠΊΠΈ.
ΠΠ½Π΅Π΄ΡΠ΅Π½ΠΈΠ΅ Π·Π°ΠΏΠΈΡΠ΅ΠΉ Π±Π»ΠΎΠ³Π°
ΠΠ°Π½Π½ΡΠ΅ Π΄Π»Ρ Π·Π°ΠΏΠΈΡΠ΅ΠΉ Π±Π»ΠΎΠ³Π° ΠΏΠ΅ΡΠ΅Π΄Π°ΡΡΡΡ Π² 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 ΠΎΡΠ»ΠΈΡΠΈΠΉ ΠΈ ΠΏΠΎΠ»ΡΡΠΈΡΠ΅ ΡΠ²ΠΎΠΉ ΠΏΡΠΈΠ·! π