Logo NFU StudioNFU Logo icon
wr贸膰 do listy
Kodowe

/ Contentlayer z Next'em

Next.js jest absolutym 馃悙 je艣li chodzi o statyczne tre艣ci. Wymieszajmy to z wygod膮 tworzenia ustrukturyzowanych danych i Markdown'em - wyjdzie nam co艣 niesamowicie skutecznego. Sprawdzimy jak i po co u偶ywa膰 tego zestawu przy u偶yciu biblioteki Contentlayer.
Arek Chwedczuk
7 minut czytania

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膰 tu npm czy yarn.

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.

posts/testowy-post.mdx
---
title: Post testowy
date: 2024-02-29
---
**Tre艣膰** MDX'a sobie zostawimy for (tzw.) fun!
posts/testowy-post-inny.mdx
---
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:

tsconfig.json
{
  "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膮:

app/page.tsx
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
app/posts/[slug]/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:

app/layout.tsx
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!

Arek Chwedczuk

jeszcze jeden i starczy...

Projekty

/ Projekt strony internetowej - wa偶niejszy ni偶 implementacja?

Po co jest faza projektowa strony internetowej i co powinno zosta膰 ustalone przed rozpocz臋ciem implementacji. Jak projektowa膰 偶eby to mia艂o r臋ce i nogi a do tego przynosi艂o firmie zyski.
Kacper Saweczko
Przeczytaj post - Projekt strony internetowej - wa偶niejszy ni偶 implementacja?

Design

/ Auto Layout w Figmie - wprowadzenie na dobry pocz膮tek

Je艣li chcesz szybciej i przyjemniej projektowa膰 w Figmie to zapoznaj si臋 jak u偶ywa膰 Auto Layout.
Joanna Wasiluk
Przeczytaj post - Auto Layout w Figmie - wprowadzenie na dobry pocz膮tek

Kodowe

/ Pojedynek Chatbot'贸w - czyli nie tylko GPT

Kr贸tkie por贸wnanie najwi臋kszych chatbot'贸w pod k膮tem u偶ycia przez zwyk艂ego Kowalskiego.
Kacper Saweczko
Przeczytaj post - Pojedynek Chatbot'贸w - czyli nie tylko GPT