Design
Contentlayer z Next'em

INFO: Contentlayer nie jest ju偶 aktywnie rozwijany.
S艂owo wst臋pu
W NFU zawsze stawiali艣my na rozwi膮zania skuteczne i dopasowane do naszych mo偶liwo艣ci. Kiedy艣 zdarza艂o si臋, 偶e machina Wordpressa "zjada艂a" nasze rozwi膮zania, wygod膮 i niezno艣n膮 popularno艣ci膮 bytu 漏. Bacznie rozgl膮daj膮c si臋 na rynku - wyr贸偶niamy dwa podej艣cia, kt贸re pozwalaj膮 uzyska膰 elastyczno艣膰 tre艣ci na stronie, a jednocze艣nie nie wymagaj膮 serwera PHP.
- systemy headless CMS ("bezg艂owy system zarz膮dzania tre艣ci膮") - co艣 takiego jak panel admina Wordpressa, ale bez "fasady". CMS udost臋pnia wtedy tylko spos贸b, 偶eby dane z niego wyci膮gn膮膰, a prezentacja pozostaje w naszych r臋kach.
- tworzenie tre艣ci w kodzie - czyli ju偶 na poziomie strony my jako tworz膮cy dodajemy pliki, kt贸re podczas np. budowania projektu stworz膮 nam struktur臋 tre艣ci.
Oba rozwi膮zania pomimo tego, 偶e efekt osi膮gaj膮 taki sam, s膮 jak 艂y偶ka i widelec. Nie mo偶emy udost臋pni膰 klientowi kodu do edycji, bo prawdopodobnie si臋 przestraszy. Nikt nie musi umie膰 programowa膰 i formatowa膰 tre艣ci samymi znakami w tek艣cie - zwykli艣my raczej do Worda i "bogatych" edytor贸w tekstu. Wi臋c klientom dajemy CMSa. Z kolei my techniczni, stroni膮cy od myszki i wychodzenia z terminala - nie wychodzimy z terminala!
Contentlayer - SDK do contentu
Contentlayer - jak na stronie okre艣lili - zestaw do tworzenia tre艣ci. Biblioteka pozwala nam:
- utworzy膰 "interfejs"/umow臋 jak maj膮 by膰 tre艣ci strukturyzowane,
- umieszcza膰 i kontrolowa膰 obok kodu,
- generowa膰 u偶yteczne w kodzie gotowe dane.
Next.js + Contentlayer = 馃殰
Zr贸bmy co艣 konkretnego.
Instalacja
Zaczniemy od nowego projektu z Next'em.
bunx create-next-app@latest
Wszystkie opcje zostawiamy domy艣lne. Zainstalujmy contentlayer'a:
bun add contentlayer
Ja u偶ywam
bun
, ale nic nie stoi na przeszkodzie, 偶eby wstawi膰 tunpm
czyyarn
.
W bazowym template od Next'a dostajemy tailwind'a i nie b臋dziemy dodawa膰 niczego bardziej ambitnego.
Konfiguracja Contentlayer'a
Na pocz膮tku musimy utworzy膰 plik contentlayer.config.ts
touch contentlayer.config.ts
Ten plik to 藕r贸d艂o naszej prawdy dot. contentlayera. Szczeg贸艂y i wszystkie opcje dost臋pne s膮 w dokumentacji. My postaramy si臋 wspomnie膰 o tym co najwa偶niejsze.
Plik contentlayer.config.ts
musi eksportowa膰 wynik funkcji makeSource
, kt贸r膮 importujemy z biblioteki.
import { makeSource } from 'contentlayer/source-files'
export default makeSource({})
W najprostszej wersji musimy doda膰 w parametrze makeSource
dwa pola:
- contentDirPath - czyli adres folderu z zawarto艣ci膮 strony
- documentTypes - tablic臋 z naszymi typami dokument贸w.
Typy dokument贸w to ju偶 konwencja contentlayera - odnosi si臋 ona do konkretnych i okre艣lonych za pomoc膮
funkcji defineDocumentType
naszych "dokument贸w", a og贸lniej m贸wi膮c tre艣ci. To za pomoc膮 tej funkcji
tworzymy "umow臋" z bibliotek膮 - czego i gdzie ma si臋 spodziewa膰, 偶eby mog艂a dzia艂a膰.
Dodajmy wi臋c nasz typ dokumentu - zwyk艂y post na bloga.
import { makeSource, defineDocumentType } from 'contentlayer/source-files'
const BlogPost = defineDocumentType(() => ({
name: "BlogPost", // nazwa do rozr贸偶niania
filePathPattern: "**/*.mdx", // glob do wyszukiwania i 艂apania plik贸w
contentType: "mdx", // typ danych w plikach
fields: {
title: {
type: "string",
required: true
},
date: {
type: "date",
required: true
}
},
computedFields: {
url: {
type: 'string',
resolve: (doc) => `posts/${doc._raw.flattenedPath}`
}
}
}))
export default makeSource({
contentDirPath: "posts",
documentTypes: [BlogPost]
})
Zadzia艂o si臋, ale to nic czego nie ma w dokumentacji.
Podali艣my wymagane pola przez funkcj臋 defineDocumentType
:
name
- nazw臋 dokumentu,filePathPattern
- 艣cie偶k臋 (tutaj globa) w kt贸rej biblioteka ma szuka膰 plik贸w z postami,contentType
- typ danych w plikach (co to MDX to temat na innego posta)fields
- konfiguracj臋 konkretnych p贸l, kt贸re musz膮 si臋 pojawi膰, 偶eby contentlayer zrobi艂 z nich posta.computedFields
- r贸wnie偶 konfiguracja p贸l, z t膮 r贸偶nic膮, 偶e te pola b臋d膮 automatycznie tworzone przez contentlayera.
Teraz jak spr贸bujemy uruchomi膰 nasz projekt - to nie powinno si臋 nic sta膰! Dopiero zadeklarowali艣my w jaki spos贸b nasze dane b臋d膮 wygl膮da膰. Musimy jeszcze je utworzy膰, a potem wy艣wietli膰... Oczywi艣cie te偶, nie damy rady od tak po prostu - przecie偶 planujemy tworzy膰 je w MDX, a on wymaga przetworzenia pliku.
Zacznijmy od stworzenia folderu posts
zgodnie z configiem.
mkdir posts
Utw贸rzmy dwa posty w tym folderze:
cd posts
touch testowy-post.mdx
touch testowy-post-inny.mdx
Teraz musimy doda膰 tre艣膰. WA呕NE - tre艣膰 musi by膰 zgodna z "umow膮".
Nasze pola dodatkowe poza tre艣ci膮, opisane polem fields
b臋d膮 dodawane tak jak frontmatterem.
---
title: Post testowy
date: 2024-02-29
---
**Tre艣膰** MDX'a sobie zostawimy for (tzw.) fun!
---
title: Post testowy inny
date: 2024-02-29
---
**Nie b臋dziemy si臋 hamowa膰 z testowaniem!**
Skoro posty mamy gotowe musimy teraz uruchomi膰 contentlayera, 偶eby wygenerowa艂 nam dane = po to jest 馃敟
Zak艂adaj膮c, 偶e korzystamy z Next'a - a do niego jest contentlayer musimy w next.config.js
albo next.config.mjs
doda膰 plugin contentlayera - wtedy b臋dzie on dzia艂a艂 razem z serverem nexta.
Osobi艣cie przenosz臋 co mog臋 na modu艂y wi臋c my skonfigurujemy next.config.mjs
u偶ywaj膮c te偶 hack'a
z shadcn-ui - bo contentlayer nie by艂 dawno aktualizowany i jest w CJS na ten moment.
Zainstalujmy plugin do Next'a:
bun add next-contentlayer
M贸j projekt na wersji Next'a 14.1.0 utworzy艂 si臋 z next.config.mjs
wi臋c musimy tylko go zmieni膰:
import { createContentlayerPlugin } from 'next-contentlayer'
const withContentLayer = createContentlayerPlugin({})
/** @type {import('next').NextConfig} */
const nextConfig = {};
// mogliby艣my skorzysta膰 z jakiej艣 funkcji `pipe` gdyby艣my mieli du偶o plugin贸w
// teraz zostaniemy przy takiej wersji
export default withContentLayer(nextConfig);
Po tym wszystkim powinni艣my m贸c zobaczy膰, 偶e contentlayer dzia艂a - uruchommy serwer dev'a:
bun dev
W konsoli je艣li wszystko si臋 uda艂o - po webpackowym i nextowym spamie - powinien by膰 te偶 log z contentlayera
Generated 2 documents in .contentlayer
Dla naszej wygody dodajmy te偶 alias, 偶eby m贸c 艂atwo dosta膰 si臋 do wygenerowanych przez contentlayera danych:
{
"compilerOptions": {
// (...) pomijamy dla czytelno艣ci
"baseUrl": ".", // <---
"paths": {
"@/*": ["./*"],
"contentlayer/generated": ["./.contentlayer/generated"] // <---
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
WA呕NE Ostatni膮 rzecz膮 b臋dzie zgodnie z dokumentacj膮 dodanie folderu
.contentlayer
do.gitignore
.
Poka偶my nasze tre艣ci
Teraz ju偶 tylko fajerwerki. Zr贸bmy tak, 偶e na stronie g艂贸wnej poka偶emy posty i zrobimy podstron臋 dla ka偶dego posta. Nie b臋dziemy te偶 przesadnie komplikowa膰 kodu - robimy prosty przyk艂ad.
Zmie艅my najpierw stron臋 g艂贸wn膮:
import { allBlogPosts, BlogPost } from "contentlayer/generated";
import Link from "next/link";
const PostCard = ({ blogPost }: { blogPost: BlogPost }) => {
return (
<div className="bg-gray-950 relative p-4 rounded-lg flex">
<div>
<h3 className="font-semibold text-xl">{blogPost.title}</h3>
<p className="text-gray-400">
{new Date(blogPost.date).toLocaleDateString()}
</p>
</div>
<Link href={blogPost.url} className="absolute size-full"></Link>
</div>
);
};
export default function Home() {
return (
<section>
<h2 className="font-bold text-3xl border-b">Posty</h2>
<div className="grid lg:grid-cols-2 my-4 gap-4">
{allBlogPosts.map((post) => (
<PostCard blogPost={post} key={post._id} />
))}
</div>
</section>
);
}
Jedyne ciekawostki to importy z contentlayer/generated
mamy tam:
- typ: BlogPost - z definicj膮 Typescriptowego typu
- allBlogPosts - ca艂a kolekcja naszych post贸w w postaci tablicy (dok艂adnie to
BlogPost[]
)
Zostaje nam tylko stworzenie podstrony z wpisem.
B臋dziemy korzysta膰 z generowanego pola _raw_flattenedPath
, kt贸re pos艂u偶y nam z "slug".
Stw贸rzmy wi臋c nasz膮 podstron臋:
cd app
mkdir posts
mkdir [slug]
touch page.tsx
import { allBlogPosts } from "@/.contentlayer/generated";
import { getMDXComponent } from "next-contentlayer/hooks";
import { notFound } from "next/navigation";
interface PostPageProps {
params: { slug: string };
}
const PostPage = ({ params: { slug } }: PostPageProps) => {
const post = allBlogPosts.find((post) => post._raw.flattenedPath === slug);
if (!post) return notFound();
const Content = getMDXComponent(post.body.code);
return (
<article>
<header>
<h1>{post.title}</h1>
<time dateTime={post.date}>
{new Date(post.date).toLocaleDateString()}
</time>
</header>
<Content />
</article>
);
};
export default PostPage;
呕eby poflexowa膰 si臋 layoutami przenios艂em kawa艂ek kodu do g艂贸wnego layoutu:
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<main className="min-h-dvh container mx-auto">
<header className="min-h-32 flex items-center justify-center">
<h1 className="font-bold text-6xl">Contentlayer z Nextem</h1>
</header>
{children}
</main>
</body>
</html>
);
}
Wrap-up
Postawili艣my projekt, z bardzo bazow膮 struktur膮 i ca艂ym content-layerem.
Trzebaby tu jeszcze doda膰 sporo rzeczy, nawet z samego przyk艂adowego projektu jak generateStaticParams
, ale
to zostawiam ambitnym czytelnikom.
G艂贸wny projekt przyk艂adowy od contentlayera: LINK
Kod do tego projektu znajduje si臋 pod LINK - wybaczcie pushowanie w maina.
Na dzie艅 29-02-2024 widzia艂em na githubie komentarz tw贸rcy, 偶e mo偶e biblioteka b臋dzie przez kogo艣 przejmowana!
jeszcze jeden i starczy...


Kodowe
Kompozycja Reactowa czyli elastyczno艣膰 ponad przewidywalno艣膰.

Projekty