Skip to content
Lucas Caton

Tutorial: Criando um blog com Next.js

Usaremos React, Tailwind CSS e posts em Markdown

Lucas Caton

Lucas Caton

@lucascaton
Next.js é tão sensacional que meio que virou meu framework padrão para novos projetos. Além disso, eu também migrei vários outros projetos, include esse site/blog que você está lendo nesse exato momento, conforme documentei aqui.
Nesse tutorial, vamos desenvolver um blog com Next.js, React e Tailwind CSS. Os posts ser√£o escritos em Markdown e convertidos para HTML no final.
E a√≠, bora codar? ūüėČ

Criando e configurando o projeto

Antes de continuar, você precisa ter o Node.js e o Yarn instalados.
Abra o terminal e rode o seguinte comando para criar o projeto:
bash
yarn create next-app -e with-tailwindcss blog
Agora, acesse o diretório do projeto e adicione as bibliotecas que vamos precisar:
bash
cd blog
yarn add react-markdown gray-matter @tailwindcss/typography
Essas bibliotecas servem para:
  • React Markdown: converter Markdown para HTML.
  • Gray Matter: interpretar front matters (veremos mais sobre isso a seguir).
  • Plugin "typography" do Tailwind CSS: estiliza√ß√£o b√°sica decente para artigos/posts.

Escrevendo o post inicial

Crie um diret√≥rio posts na raiz do projeto e dentro, crie um arquivo chamado hello-world.md com o conte√ļdo que voc√™ desejar:
markdown
---
title: "Hello World"
date: "07/12/2021"
---

Ol√° mundo!

**Texto em negrito** e _texto em it√°lico_.

- Lista
- com
- v√°rios
- items

O que é "Front Matter"?

O bloco no começo de arquivos markdowns, entre os traços (---) é o que chamamos de "Front Matter" e é onde geralmente definimos metadados do post. Esse bloco precisa necessariamente estar no começo do arquivo e precisa usar uma sintaxe YAML válida.
yml
title: "Hello World"
date: "07/12/2021"

Preparando a p√°gina inicial

Vamos mover o componente <Head> do arquivo pages/index.js para pages/_app.js e renomear o conte√ļdo da tag <title>:

pages/_app.js

jsx
import Head from "next/head";
import "tailwindcss/tailwind.css";

function MyApp({ Component, pageProps }) {
  return (
    <>
      <Head>
        <title>Meu blog</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <Component {...pageProps} />
    </>
  );
}

export default MyApp;
Depois vamos deixar apenas um título Posts na página inicial:

pages/index.js

jsx
const Blog = () => {
  return <h1>Posts</h1>;
};

export default Blog;

Conferindo se está tudo bem até aqui

Vamos rodar o servidor para ver se está tudo certo até agora. No terminal, rode:
bash
yarn dev
Abra o navegador e acesse localhost:3000. Você deve ver algo assim:
Vers√£o inicial do blog
Vers√£o inicial do blog

Criando e estilizando o cabeçalho

Vamos adicionar as tags <header> e <main> ao arquivo pages/_app.js com algumas classes do Tailwind CSS:
diff
 import Head from "next/head";
+import Link from "next/link";
+
 import "tailwindcss/tailwind.css";

 function MyApp({ Component, pageProps }) {
@@ -9,7 +11,17 @@ function MyApp({ Component, pageProps }) {
         <link rel="icon" href="/favicon.ico" />
       </Head>

-      <Component {...pageProps} />
+      <header className="py-10 bg-gradient-to-r from-green-400 to-blue-500 text-center">
+        <Link href="/">
+          <a>
+            <h2 className="text-5xl font-bold text-white">Meu blog</h2>
+          </a>
+        </Link>
+      </header>
+
+      <main className="my-6 mx-auto p-6 bg-white sm:shadow-lg rounded prose lg:prose-xl">
+        <Component {...pageProps} />
+      </main>
     </>
   );
 }

Personalizando a tipografia do Tailwind CSS

Vamos extender alguns comportamentos padr√Ķes de tipografia do Tailwind CSS e carregar o plugin oficial @tailwindcss/typography.
Para isso, abra o arquivo tailwind.config.js e adicione isso:
diff
   darkMode: false, // or 'media' or 'class'
   theme: {
-    extend: {},
+    extend: {
+      typography: {
+        DEFAULT: {
+          css: {
+            a: {
+              color: "#3182ce",
+              "&:hover": {
+                color: "#2c5282",
+              },
+            },
+          },
+        },
+      },
+    },
   },
   variants: {
     extend: {},
   },
-  plugins: [],
+  plugins: [require("@tailwindcss/typography")],
 };

Adicionando classes CSS na tag <body>

Para fazer isso, precisamos criar um arquivo pages/_document.js com o seguinte conte√ļdo:
jsx
import Document, { Html, Head, Main, NextScript } from "next/document";

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    return { ...initialProps };
  }

  render() {
    return (
      <Html>
        <Head />
        <body className="bg-white sm:bg-gray-50">
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;
Repare que eu adicionei as classes bg-white e sm:bg-gray-50 na tag <body>.
Veja mais detalhes sobre pages/_document.js na documentação oficial.

Lendo o conte√ļdo dos posts

Crie um arquivo lib/posts.js que ser√° respons√°vel por ler os arquivos Markdown e retornar uma lista de posts:
js
import { promises as fs } from "fs";
import path from "path";
import matter from "gray-matter";

const getPosts = async () => {
  const postsDirectory = path.join(process.cwd(), "posts");
  const filenames = await fs.readdir(postsDirectory);

  return await Promise.all(
    filenames.map(async (filename) => {
      const filePath = path.join(postsDirectory, filename);
      const fileContents = await fs.readFile(filePath, "utf8");
      const document = matter(fileContents);

      return {
        slug: filename.replace(/\.md$/, ""),
        title: document.data.title,
        date: document.data.date,
        markdown: document.content,
      };
    })
  );
};

export default getPosts;
Cada post é representado por um objeto com as seguintes chaves:
  • slug: Representa√ß√£o do post que pode ser usada na URL. Para isso, vamos usar o nome do arquivo sem sua extens√£o (.md).
  • title: Metadado definido no front matter
  • date: Metadado definido no front matter
  • markdown: Conte√ļdo principal do arquivo Markdown
Repare que estamos usando a biblioteca gray-matter para ler os metadados definidos no front matter de cada post.

Adicionando os posts à página inicial

Vamos importar a função getPosts do arquivo que acabamos de criar na página inicial, ou seja, no arquivo pages/index.js e fazer um loop para exibir links e títulos de cada um dos posts:
jsx
import Link from "next/link";

import getPosts from "../lib/posts";

const Blog = ({ posts }) => {
  return (
    <>
      <h1>Posts</h1>
      <ul>
        {posts.map(({ slug, title }) => (
          <li key={slug}>
            <Link href={`/${slug}`}>
              <a>{title}</a>
            </Link>
          </li>
        ))}
      </ul>
    </>
  );
};

export async function getStaticProps() {
  return {
    props: {
      posts: await getPosts(),
    },
  };
}

export default Blog;
Vamos ver no navegador como nosso blog est√° ficando:
Lista de posts
Lista de posts
Massa!

Criando as p√°ginas dos posts com rotas din√Ęmicas

Ao tentar clicar no link do post Hello World que vemos da p√°gina acima, um erro 404 (p√°gina n√£o encontrada) ser√° mostrada. Isso √© esperado, afinal, ainda n√£o criamos essa p√°gina ūüôÉ
Bora cri√°-la, ent√£o. Como queremos um mesmo template para os posts, mas com rotas din√Ęmicas (baseadas no nome de seus arquivos), vamos criar e nomear o arquivo dos posts com colchetes: pages/[slug].js.
Seu conte√ļdo pode ser algo mais ou menos assim:
jsx
import getPosts from "../lib/posts";

const Post = ({ title, date, markdown }) => (
  <article>
    <h1>{title}</h1>
    <time className="font-extralight tracking-wider text-gray-500">{date}</time>
    {markdown}
  </article>
);

export const getStaticPaths = async () => {
  const posts = await getPosts();

  return {
    paths: posts.map((post) => `/${post.slug}`),
    fallback: false,
  };
};

export const getStaticProps = async ({ params: { slug } }) => {
  const posts = await getPosts();
  const post = posts.find((post) => post.slug === slug);

  return { props: post };
};

export default Post;
Agora vamos verificar como ficou no navegador...
P√°gina do post
P√°gina do post
Ok, est√° ficando legal, mas ainda falta converter o Markdown.

Convertendo o Markdown para HTML

Vamos usar a bilioteca ReactMarkdown para fazer isso. Atualize o arquivo pages/[slug].js para:
diff
+import ReactMarkdown from "react-markdown";
+
 import getPosts from "../lib/posts";

 const Post = ({ title, date, markdown }) => (
   <article>
     <h1>{title}</h1>
     <time className="font-extralight tracking-wider text-gray-500">{date}</time>
-    {markdown}
+    <ReactMarkdown>{markdown}</ReactMarkdown>
   </article>
 );
Vamos testar novamente:
P√°gina do post com Markdown convertido
P√°gina do post com Markdown convertido
Ah, bem melhor!

Adicionando um favicon

Para finalizar, substitua o favicon da pasta public/.
Se você não tiver um, pode usar esse:
Fique a vontade também para remover o arquivo public/vercel.svg, que atualmente não tem utilidade nenhuma.

Resultado final ūüéČ

Blog - vers√£o final
Blog - vers√£o final
Se quiser aprender tudo sobre React e Next.js, d√™ uma olhada no meu curso de React ūüėČ

Vídeo que originou esse post

Repositório no GitHub

O c√≥digo do blog desenvolvido nesse tutorial est√° publicado integralmente no GitHub, caso voc√™ precise por qualquer motivo! ūüėČ

O que achou?

Me conte nos coment√°rios abaixo!