Criando um Blog com contador de visitas usando NextJS e MongoDB

Neste post vamos aprender a criar um Blog com NextJS, usando o MongoDB para gerenciar um contador de visitas em cada post e exibir no preview da home page. Usaremos a Fetch API para buscar os dados e o SWR para nos auxiliar nas revalidações dos mesmos. No final vamos hospedar em produção usando a Vercel.

📝 Pré-requisitos

Sempre queremos entregar a melhor experiência para nossa audiência.

Esse post vai ser um pouco mais complexo, fique a vontade para ler e seguir os passos. Se você tiver conhecimento prévio vai ficar mais tranquilo, também temos vários conteúdos no YouTube e no blog sobre os assuntos abaixo:

🏛️ Introdução

A arquitetura do NextJS é robusta, feita para criar aplicações em React. A API Routes permite criar API com NextJS, onde o NodeJS brilha ao rodar por baixo dos panos. Com isso podemos usar o MongoDB em uma aplicação usando um Front End NextJS.

Criação de blogs é um bom exemplo para o uso do NextJS. Ele tem seu próprio gerenciador de rotas, baseado em sistema de arquivos construído no conceito de páginas. Podemos criar rotas dinâmicas também.

A comunidade do NextJS abraçou o framework e vem criando vários exemplos e boilerplates bem interessantes para se basear e aprender a construir aplicações.

Para implementar nosso caso de uso vamos usar dois exemplos dos links abaixo:

No código de exemplo já tem um blog bem bacana feito com NextJS, TypeScript, usando Tailwind para estilização e Markdown para criação estática dos posts.

Vantagem dessa estrutura é que você não vai precisar de um Back End para buscar os posts (API NodeJS, Prismic, Ghost, etc). A desvantagem é que a leitura de todos os posts de dentro do código vai ficar lenta, com isso se seu plano de hospedagem na Vercel ou Netlify for gratuita, você terá que migrar, mas também há outras soluções.

Passo 1 - Baixando o projeto

Comece baixando o projeto na sua workstation — pasta onde você deixa seus códigos:

npx create-next-app --example blog-starter-typescript blog-rocketseat
// ou
yarn create next-app --example blog-starter-typescript blog-rocketseat

Esse comando vai baixar o boilerplate do blog. Você pode dar o nome que quiser, basta trocar ali o blog-rocketseat.

Passo 2 - Estrutura inicial do Blog

O projeto do blog tem a seguinte estrutura:

blog-rocketseat on  master took 3s 
❯ tree -h            
.
├── [  96]  @types
│   └── [  29]  remark-html.d.ts
├── [2.2K]  README.md
├── [ 160]  _posts
│   ├── [2.2K]  dynamic-routing.md
│   ├── [2.2K]  hello-world.md
│   └── [2.2K]  preview.md
├── [ 640]  components
│   ├── [1.2K]  alert.tsx
│   ├── [ 322]  avatar.tsx
│   ├── [ 253]  container.tsx
│   ├── [ 639]  cover-image.tsx
│   ├── [ 278]  date-formatter.tsx
│   ├── [1.2K]  footer.tsx
│   ├── [ 307]  header.tsx
│   ├── [1.2K]  hero-post.tsx
│   ├── [ 681]  intro.tsx
│   ├── [ 407]  layout.tsx
│   ├── [ 251]  markdown-styles.module.css
│   ├── [1.2K]  meta.tsx
│   ├── [ 783]  more-stories.tsx
│   ├── [ 352]  post-body.tsx
│   ├── [ 961]  post-header.tsx
│   ├── [ 981]  post-preview.tsx
│   ├── [ 333]  post-title.tsx
│   └── [ 124]  section-separator.tsx
├── [ 160]  lib
│   ├── [1.1K]  api.ts
│   ├── [ 327]  constants.ts
│   └── [ 214]  markdownToHtml.ts
├── [  75]  next-env.d.ts
├── [ 715]  package.json
├── [ 192]  pages
│   ├── [ 174]  _app.tsx
│   ├── [ 290]  _document.tsx
│   ├── [1.3K]  index.tsx
│   └── [  96]  posts
│       └── [2.3K]  [slug].tsx
├── [  71]  postcss.config.js
├── [ 128]  public
│   ├── [  96]  assets
│   │   └── [ 192]  blog
│   │       ├── [ 160]  authors
│   │       │   ├── [6.0K]  jj.jpeg
│   │       │   ├── [7.0K]  joe.jpeg
│   │       │   └── [6.0K]  tim.jpeg
│   │       ├── [  96]  dynamic-routing
│   │       │   └── [115K]  cover.jpg
│   │       ├── [  96]  hello-world
│   │       │   └── [103K]  cover.jpg
│   │       └── [  96]  preview
│   │           └── [ 43K]  cover.jpg
│   └── [ 384]  favicon
│       ├── [4.7K]  android-chrome-192x192.png
│       ├── [ 14K]  android-chrome-512x512.png
│       ├── [1.3K]  apple-touch-icon.png
│       ├── [ 255]  browserconfig.xml
│       ├── [ 595]  favicon-16x16.png
│       ├── [ 880]  favicon-32x32.png
│       ├── [ 15K]  favicon.ico
│       ├── [3.5K]  mstile-150x150.png
│       ├── [1.9K]  safari-pinned-tab.svg
│       └── [ 392]  site.webmanifest
├── [  96]  styles
│   └── [ 276]  index.css
├── [ 692]  tailwind.config.js
├── [ 484]  tsconfig.json
├── [ 128]  types
│   ├── [  74]  author.ts
│   └── [ 229]  post.ts
└── [275K]  yarn.lock

16 directories, 55 files

Este blog é bem robusto e tem uma code base bem grandinha. Mas não se assuste, eu mesmo não li todo o código. Você não precisa conhecer cada arquivo e linha de código desse projeto para continuar.

Passo 3 - Instalando as Dependências e inicializando o Projeto

Execute yarn install só para garantir que a node_modules já está no projeto com as dependências instaladas.

Depois execute yarn dev para executar a aplicação e você terá o seguinte resultado:

blog aparecendo após a execução do comando: yarn dev

Navegue entre os posts, dê uma fuçada nas páginas e fique à vontade para ver a estrutura do projeto.

Seguindo, vamos instalar as dependências necessárias. Basicamente são duas:

  • mongodb e @types/mongodb (driver de conexão com MongoDB para aplicações NodeJS)
  • swr (stale-while-revalidate — hook para busca de dados)
yarn add mongodb swr
yarn add @types/mongodb -D

Passo 4 - Configurando a conexão com Mongo Atlas

Vamos configurar a conexão com o banco de dados no Atlas. Antes disso você precisa criar sua conta gratuita no serviço deles e criar um projeto — Projeto → Cluster → Collection.

Adicionar IP Access List Entry: 0.0.0.0/0 (pode colocar o endereço onde o app está executando — caso esteja hospedado)

Adicionando IP nas configurações do MondoDB

E também precisa de um usuário e senha para se conectar ao banco de dados:

Criando uma conta de usuário para acessar o banco de dados do MongoDB.

Depois você vai precisar obter e copiar a string de conexão com mongoDB:

mongodb+srv://meu_usuario:<password>@cluster.kwhje.mongodb.net/<dbname>?retryWrites=true&w=majority

Para conseguir essa string, acesse: Clusters → Connect → Connect your application (segunda opção) → Copy. Pronto!

Obtendo a string de conexão do Cluster (database) MongoDB

Esses dados são sensíveis, então vamos precisar configurar variáveis de ambiente para salva-los.

Na raiz do projeto crie o arquivo .env.local :

MONGODB_URI=mongodb+srv://meu_usuario:<password>@cluster.kwhje.mongodb.net/<dbname>?retryWrites=true&w=majority
MONGODB_DB=blog_post_page_view # aqui é o nome do banco de dados

Com banco de dados e string de conexão na variável de ambiente, podemos avançar e configurar o driver de conexão entre MongoDB ↔ NodeJS.

Passo 5 - Configurando o driver de conexão do MongoDB

Na raiz do projeto crie uma pasta config e o arquivo mongodb.ts. Nele adicione esse código:

import { MongoClient } from 'mongodb'

let uri = process.env.MONGODB_URI || "" // trick ts :(
let dbName = process.env.MONGODB_DB

let cachedClient: any = null
let cachedDb: any = null

if (!uri) {
  throw new Error(
    'Please define the MONGODB_URI environment variable inside .env.local'
  )
}

if (!dbName) {
  throw new Error(
    'Please define the MONGODB_DB environment variable inside .env.local'
  )
}

export async function connectToDatabase() {
  if (cachedClient && cachedDb) {
    return { client: cachedClient, db: cachedDb }
  }

  const client = await MongoClient.connect(uri, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })

  const db = await client.db(dbName)

  cachedClient = client
  cachedDb = db

  return { client, db }
}

Esse código lida com a conexão com o MongoDB, usando as variáveis de ambiente que definimos, e também gerencia o cache da conexão.

A cada requisição, ao invés de criar uma nova conexão, o que demanda memória e tempo de comunicação entre a aplicação e o mongodb, ela simplesmente retorna a conexão existente. ;)

Passo 6 - Construindo a ponte entre o MongoDB e o NextJS — API Router

Vamos criar agora a API NextJS que será chamada pelos componentes React e que irá realizar a busca de dados no MongoDB.

Note que todos os arquivos começados com _, como _app.tsx por exemplo, não são considerados rotas na aplicação.

No arquivo tsconfig.json configure os módulos, isso vai facilitar muito na hora de importar os arquivos:

{
"baseUrl": "./",
    "paths": {
      "@/*": ["./*"]
    }
}

Seu arquivo deve ficar assim no final:

{
  "compilerOptions": {
    "target": "es5",
    "module": "esnext",
    "jsx": "preserve",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "noEmit": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "baseUrl": "./",
    "paths": {
      "@/*": ["./*"]
    }
  },
  "exclude": ["node_modules"],
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}

Agora podemos importar um arquivo dessa maneira:

import {connectToDatabase} from '@/config/mongodb';

Ao invés de:

import {connectToDatabase} from '../../../config/mongodb';

Muito mais prático, né? ;)

Nossa API tem que estar na pasta pages dentro da pasta api. Crie a pasta api dentro da pasta pages e um arquivo page-views.ts com o seguinte código:

import {connectToDatabase} from '@/config/mongodb';
import { NextApiRequest, NextApiResponse } from 'next';

export default async (req: NextApiRequest, res: NextApiResponse) => {
  
  const slug = req.query.id; 
  
  if(!slug) return res.json("Página não encontrada!")

  const { db, client } = await connectToDatabase();

  if(client.isConnected()) {
    const pageViewBySlug = await db
    .collection("pageviews")
    .findOne({ slug })

    let total = 0;
    if(pageViewBySlug) {
      total = pageViewBySlug.total + 1;
      await db.collection('pageviews').updateOne({ slug }, { $set: { total }})
    } else {
      total = 1;
      await db.collection('pageviews').insertOne({ slug, total })
    }
    
    return res.status(200).json({ total })
    
  }

  return res.status(500).json({ error: 'client DB is not connected' })

}

Esse arquivo se parece muito com uma rota no server NodeJS com Express, que recebe requisição e envia resposta.

// https://blog.rocketseat.com.br/tipos-de-parametros-nas-requisicoes-rest/
server.get("/users", (req, res) => {
 const name = req.query.name;	
 return res.json({ message: `Hello ${name}` });
});

E é isso que ele faz, podemos ver ele expondo uma função que recebe requisições e devolve alguma resposta no retorno da função.

Essa função basicamente recebe um slug (título do blog — slugify) como parâmetro. Primeiro verifique se existe o slug no banco de dados. Se existir pegue o total e acrescente mais um e salve esse valor na coleção pageviews, onde o slug é igual ao slug informado. Se não existir, retorne um total igual a um, ou seja, primeira visita. Em seguida vai salvar o registro, a resposta vai ser o total de visitas que o post teve ;)

Essa rota da API page-views será chamada sempre que entrar no post do blog.

Mas esse blog tem um preview de posts, então vamos criar uma rota que vai servir só para página home do blog e exibir o total de visitas em cada post.

Crie o arquivo page-views-preview.ts na pasta api com o seguinte conteúdo:

import {connectToDatabase} from '@/config/mongodb';
import { NextApiRequest, NextApiResponse } from 'next';

export default async (req: NextApiRequest, res: NextApiResponse) => {
  
  const slug = req.query.id; 
  
  if(!slug) return res.json("Página não encontrada!")

  const { db, client } = await connectToDatabase();

  if(client.isConnected()) {
    const pageViewBySlug = await db
    .collection("pageviews")
    .findOne({ slug })

  let total = 0;
  if(pageViewBySlug) {
    total = pageViewBySlug.total;
  }
   
  return res.status(200).json({ total })
    
  }

  return res.status(500).json({ error: 'client DB is not connected' })

}

Este arquivo faz basicamente a mesma coisa que o código anterior, a diferença é que este retorna um total igual a zero se o post não teve visualizações ou retorna a quantidade total baseado no slug informado.

Disclaimer -> Esses dois códigos estão sujeitos a muitas melhorias, quis deixar o mais simples possível para entender bem o fluxo. ;)

Pronto! Temos o banco configurado e a API criada, agora falta pouco para finalizar.

Vamos implementar o acesso à API, ou seja, a parte que efetua as requisições na API.

Passo 7 - Configurando a busca de dados com SWR

Agora vamos criar o fetcher — buscador de dados avançado que será reutilizado nos components. Usaremos o SWR para gerenciar as buscas, cache e validação das consultas. Vamos usar também a Fetch API do JavaScript que efetivamente irá fazer as buscas na API quando o hook SWR solicitar.

Dentro da pasta lib que já existe no projeto, crie o arquivo fetcher.ts e adicione o código abaixo:

import useSWR from "swr";

export function useFetch(url: string, revalidateOnFocus: boolean = false) {
  const { data, error } = useSWR(url, async (url) => {
    const response = await fetch(url);
    const data = await response.json();

    return data;
  }, {revalidateOnFocus });

  return { data, error };
}

Temos um post e um vídeo só sobre SWR. Basicamente ele recebe a URL da chamada para API, e um boolean que é repassado como terceiro parâmetro do hook useSWR que revalida os dados, ou seja, refaz a busca na API quando a tela da aplicação recebe foco (clique na tela).

A URL é repassada para o useSWR, que é utilizada no fetch, a qual chama a API que implementamos nos passos 5 e 6, e retorna os dados. Os dados e erros que podem ser gerados na execução do useSWR são repassados para o retorno da função useFetch que definimos.

Toda chamada da nossa API será através desse nosso hook useFetch, onde encapsulamos a lógica da execução do useSWR e Fetch API.

Uffa, fizemos bastante coisa até aqui, para dar uma relaxada vamos "alterar label". 😂

Passo 8 - Personalizando o conteúdo estático do Blog

No arquivo components/header.tsx altere o texto de Blog para <Seu Blog>.

No arquivo components/intro.tsx altere o texto e o CSS do primeiro <h1>:

<h1 className="text-4xl md:text-5xl font-bold tracking-tighter leading-tight md:pr-8">
    Blog da Rocketseat.
</h1>

Pronto! Veja o resultado na tela ;)

Esse CSS inline é do Tailwind — framework CSS para construir designs customizáveis rapidamente. Temos um vídeo sobre libs UI declarativas no canal.

Pronto, agora chega de moleza, vamos brincar mais! :)

Passo 9 - Consumindo a API, adaptando e criando componentes

Agora vamos utilizar o data fetching!

No arquivo pages/posts/[slug].tsx vamos implementar a busca do total de page view (visitas na página).

No arquivo completo abaixo, observe como é feito a busca para dentro da própria API que construimos, passando o caminho: api/page-views e o parâmetro id={post.slug}, que vai formar uma requisição: http://localhost:3000/api/page-views?id=hello-world

  const { data } = useFetch(`/api/page-views?id=${post.slug}`);

O objeto data traz consigo a propriedade total que a API respondeu, e qual é passada para o componente PageHeader.

 <PostHeader
    title={post.title}
    coverImage={post.coverImage}
    date={post.date}
    author={post.author}
    views={data?.total}
 />

Veja o arquivo completo e substitua por esse código:

// pages/posts/[slug].tsx

import { useRouter } from "next/router";
import ErrorPage from "next/error";
import Container from "../../components/container";
import PostBody from "../../components/post-body";
import Header from "../../components/header";
import PostHeader from "../../components/post-header";
import Layout from "../../components/layout";
import { getPostBySlug, getAllPosts } from "../../lib/api";
import PostTitle from "../../components/post-title";
import Head from "next/head";
import { CMS_NAME } from "../../lib/constants";
import markdownToHtml from "../../lib/markdownToHtml";
import PostType from "../../types/post";

import { useFetch } from "@/lib/fetcher";

type Props = {
  post: PostType;
  morePosts: PostType[];
  preview?: boolean;
};

const Post = ({ post, morePosts, preview }: Props) => {
  const router = useRouter();
  if (!router.isFallback && !post?.slug) {
    return <ErrorPage statusCode={404} />;
  }

  const { data } = useFetch(`/api/page-views?id=${post.slug}`);

  return (
    <Layout preview={preview}>
      <Container>
        <Header />
        {router.isFallback ? (
          <PostTitle>Loading…</PostTitle>
        ) : (
          <>
            <article className="mb-32">
              <Head>
                <title>
                  {post.title} | Next.js Blog Example with {CMS_NAME}
                </title>
                <meta property="og:image" content={post.ogImage.url} />
              </Head>
              <PostHeader
                title={post.title}
                coverImage={post.coverImage}
                date={post.date}
                author={post.author}
                views={data?.total}
              />
              <PostBody content={post.content} />
            </article>
          </>
        )}
      </Container>
    </Layout>
  );
};

export default Post;

type Params = {
  params: {
    slug: string;
  };
};

export async function getStaticProps({ params }: Params) {
  const post = getPostBySlug(params.slug, [
    "title",
    "date",
    "slug",
    "author",
    "content",
    "ogImage",
    "coverImage",
  ]);
  const content = await markdownToHtml(post.content || "");

  return {
    props: {
      post: {
        ...post,
        content,
      },
    },
  };
}

export async function getStaticPaths() {
  const posts = getAllPosts(["slug"]);

  return {
    paths: posts.map((posts) => {
      return {
        params: {
          slug: posts.slug,
        },
      };
    }),
    fallback: false,
  };
}

Ainda não podemos ver o resultado em tela, pois precisamos adaptar o componente page-header.tsx para receber essa nova prop que passamos — e outras coisas que veremos logo adiante. ;)

Esse projeto está bem organizado, vamos manter este padrão. Dentro da pasta components, tem vários arquivos post-body.tsx, post-title.tsx, etc. Vamos criar o arquivo post-views.tsx que, além de ser o componente responsável pela exibição da quantidade de visualizações, também vai ser reaproveitado em outros componentes: hero-post.tsx, post-header.tsx e post-preview.tsx.

Crie esse arquivo com o seguinte componente:

// components/post-views.tsx

import { ReactNode } from "react";

type Props = {
  children?: ReactNode;
};

const PostViews = ({ children }: Props) => {
  return <small className="text-lg">{children}</small>;
};

export default PostViews;

Vamos utilizar esse componente em três lugares: hero-posts.tsx que é o post principal em destaque na home page; more-stories.tsx também na home onde aparece os restantes dos previews dos posts; e, por fim, no próprio post, dentro de post-header.tsx.

Para podermos ver o efeito em tela logo, vamos colocar no post-header.tsx primeiro:

Altere o arquivo com esse conteúdo:

// components/post-header.tsx
import Avatar from "./avatar";
import DateFormatter from "./date-formatter";
import CoverImage from "./cover-image";
import PostTitle from "./post-title";
import PostViews from "./post-views";
import Author from "../types/author";

type Props = {
  title: string;
  coverImage: string;
  date: string;
  author: Author;
  views: number;
};

const PostHeader = ({ title, coverImage, date, author, views }: Props) => {
  return (
    <>
      <PostTitle>{title}</PostTitle>
      <div className="hidden md:block md:mb-12">
        <Avatar name={author.name} picture={author.picture} />
      </div>
      <div className="mb-8 md:mb-16 sm:mx-0">
        <CoverImage title={title} src={coverImage} />
      </div>
      <div className="max-w-2xl mx-auto">
        <div className="block md:hidden mb-6">
          <Avatar name={author.name} picture={author.picture} />
        </div>
        <div className="mb-6 text-lg">
          <DateFormatter dateString={date} /> -{" "}
          <PostViews>{`${views >= 0 ? views : "..."} views`}</PostViews>
        </div>
      </div>
    </>
  );
};

export default PostHeader;

Pronto, até aqui vamos ver o resultado desse nosso longo trabalho.

Acessando a rota: http://localhost:3000/posts/dynamic-routing

Assim tivemos nossa primeira visualização no post!

Eita, notou o detalhe que "views" está no plural — 1 views. Poderíamos ter colocado uma condição para mostrar view quando tem apenas 1 view, mas não vale a pena. Logo o post vai ter mais de uma view, então temos um if ternário a menos no código.

Faça um refresh na página e você vai notar que o valor irá incrementar, mas antes disso vai aparecer ... , que foi o melhor loading que já fiz na minha vida, enquanto aguarda chegar o valor total!

Vamos mostrar na home a quantidade de views de cada post.

No arquivo components/hero-post.tsx altere o código:

import Avatar from "./avatar";
import DateFormatter from "./date-formatter";
import CoverImage from "./cover-image";
import Link from "next/link";
import Author from "../types/author";
import { useFetch } from "@/lib/fetcher";
import PostViews from "./post-views";

type Props = {
  title: string;
  coverImage: string;
  date: string;
  excerpt: string;
  author: Author;
  slug: string;
};

const HeroPost = ({
  title,
  coverImage,
  date,
  excerpt,
  author,
  slug,
}: Props) => {
  
	const { data } = useFetch(`/api/page-views-preview?id=${slug}`, true);

  const views = data?.total;

  return (
    <section>
      <div className="mb-8 md:mb-16">
        <CoverImage title={title} src={coverImage} slug={slug} />
      </div>
      <div className="md:grid md:grid-cols-2 md:col-gap-16 lg:col-gap-8 mb-20 md:mb-28">
        <div>
          <h3 className="mb-4 text-4xl lg:text-6xl leading-tight">
            <Link as={`/posts/${slug}`} href="/posts/[slug]">
              <a className="hover:underline">{title}</a>
            </Link>
          </h3>
          <div className="mb-4 md:mb-0 text-lg">
            <DateFormatter dateString={date} /> -{" "}
            <PostViews>{`${views >= 0 ? views : "..."} views`}</PostViews>
          </div>
        </div>
        <div>
          <p className="text-lg leading-relaxed mb-4">{excerpt}</p>
          <Avatar name={author.name} picture={author.picture} />
        </div>
      </div>
    </section>
  );
};

export default HeroPost;

Nesse arquivo notamos duas diferenças principais: a rota que estamos requisitando e o parâmetro true sendo informado:

const { data } = useFetch(`/api/page-views-preview?id=${slug}`, true);

Aqui é onde o SWR brilha de fato e você vai curtir!

Além de estar passando a URL, que agora é page-views-preview, para acessar a outra rota da API, estou passando true como segundo parâmetro para que seja feita a revalidação dos dados quando a tela recebe um foco (clique do mouse na página home por exemplo).

Veja o resultado:

Estrutura no banco de dados no Mongo Atlas, para cada slug:

Agora para finalizar com chave de ouro, vamos adicionar o contador no restante do preview da home.

No arquivo components/post-preview.tsx altere o arquivo:

import Avatar from "./avatar";
import DateFormatter from "./date-formatter";
import CoverImage from "./cover-image";
import Link from "next/link";
import Author from "../types/author";
import PostViews from "@/components/post-views";
import { useFetch } from "@/lib/fetcher";

type Props = {
  title: string;
  coverImage: string;
  date: string;
  excerpt: string;
  author: Author;
  slug: string;
};

const PostPreview = ({
  title,
  coverImage,
  date,
  excerpt,
  author,
  slug,
}: Props) => {
  

  const { data } = useFetch(`/api/page-views-preview?id=${slug}`, true);

  const views = data?.total;

  return (
    <div>
      <div className="mb-5">
        <CoverImage slug={slug} title={title} src={coverImage} />
      </div>
      <h3 className="text-3xl mb-3 leading-snug">
        <Link as={`/posts/${slug}`} href="/posts/[slug]">
          <a className="hover:underline">{title}</a>
        </Link>
      </h3>
      <div className="text-lg mb-4">
        <DateFormatter dateString={date} /> -{" "}
        <PostViews>{`${views >= 0 ? views : "..."} views`}</PostViews>
      </div>
      <p className="text-lg leading-relaxed mb-4">{excerpt}</p>
      <Avatar name={author.name} picture={author.picture} />
    </div>
  );
};

export default PostPreview;

Segue com a mesma implementação. Simplesmente fizemos o fetch do total de visitas por slug:

const { data } = useFetch(`/api/page-views-preview?id=${slug}`, true);

E adicionamos o componente:

 <PostViews>{`${views >= 0 ? views : "..."} views`}</PostViews>

Prontinho! 💜

Vamos ver o resultado final?

Fluxo completo do Blog com o contador de visualizações da página.

Olha que massa que ficou!

Terminou? É isso?

"Localhost is cheap, show it to me in production!" - Thiago Marinho

Passo 10 - Subindo o projeto em produção na Vercel

De que adianta um blog em localhost, certo? Bora colocar em produção!

Tem duas maneiras de fazer isso usando a vercel.

Primeiro, subir o código para o GitHub — lembre de não enviar o arquivo .env.

Depois colocar o link do repositório no site da Vercel e configurar a variável de ambiente.

Segunda maneira é fazer o deploy via vercel cli.

Prefiro fazer da primeira maneira, porque gosto da integração automatizada do GitHub com a Vercel, mas aqui vou fazer da segunda maneira.

No site da vercel tem instruções de como baixar o CLI.

Basta executar o comando:

npm i -g vercel

O pacote global vai ser instalado na máquina.

Crie sua conta na vercel.  Use sua conta do GitHub para fazer o sign-up/sign-in.

Voltando para o terminal digite:

vercel login

Você vai precisar informar um e-mail, o mesmo que usou para criar a conta. Feito isso, basta clicar em confirmar quando receber.

blog-rocketseat on  main took 14s 
❯ vercel login 
Vercel CLI 20.1.2
We sent an email to vc@gmail.com. Please follow the steps provided inside it and make sure the security code matches Bla Bla.
✔ Email confirmed
Congratulations! You are now logged in. In order to deploy something, run `vercel`.
💡  Connect your Git Repositories to deploy every branch push automatically (<https://vercel.link/git>).
blog-rocketseat on  main took 40s

Para finalizar, basta digitar outro comando para fazer o deploy.

Antes recomendo gerar uma build na máquina local para ver se está tudo certo mesmo! ;)

yarn build

Se tudo deu certo, e aqui deu — na minha máquina funciona! :) Só executar o comando  yarn start que seu blog vai rodar como se estivesse em produção, ou seja, vai executar o projeto na build.

Agora, para colocar em produção na Vercel, execute o comando:

vercel

E responder as perguntas:

❯ vercel      
Vercel CLI 20.1.2
? Set up and deploy “~/Developer/blog-rocketseat”? [Y/n] y
? Which scope do you want to deploy to? Thiago Marinho
? Link to existing project? [y/N] n
? What’s your project’s name? blog-rocketseat
? In which directory is your code located? ./
Auto-detected Project Settings (Next.js):
- Build Command: `npm run build` or `next build`
- Output Directory: Next.js default
- Development Command: next dev --port $PORT
? Want to override the settings? [y/N] n
🔗  Linked to tgmarinho/blog-rocketseat (created .vercel)
🔍  Inspect: <https://vercel.com/tgmarinho/blog-rocketseat/kd9113c6m> [1s]
✅  Production: <https://blog-rocketseat.vercel.app> [copied to clipboard] [1m]
📝  Deployed to production. Run `vercel --prod` to overwrite later (<https://vercel.link/2F>).
💡  To change the domain or build command, go to <https://vercel.com/tgmarinho/blog-rocketseat/settings>
blog-rocketseat on  main took 2m 14s

É um processo muito mais simples do que qualquer outro que já tive experiência de fazer para colocar algum projeto em produção.

Pronto, blog está em produção: https://blog-rocketseat.vercel.app/

Mas as views ficam só com ..., não está funcionando! 😥

Claro, faltou configurar a variável de ambiente na vercel.

Acesse: https://vercel.com/<seu_user_vercel>/<seu_projeto>/settings/environment-variables

E insira a chave e o valor das suas variáveis que estão no .env do seu projeto.

Feito isso prometo que tudo vai funcionar!

Faça um novo deploy para que as variáveis sejam implantadas no projeto. No terminal da sua máquina digite:

vercel --prod

Agora é só aguardar e testar!

Acesse o projeto: https://blog-rocketseat.vercel.app/

Podemos brincar com a API também:

https://blog-rocketseat.vercel.app/api/page-views-preview?id=1

Você pode conferir o link do projeto no GitHub Rocketseat Content.

👍 Conclusão

O resultado ficou muito legal! E agora você tem um template bacana de um blog que você pode personalizar e começar a criar seus conteúdos. Mas, se quiser, pode criar um blog totalmente do zero usando no NextJS, até porque esse já está com Tailwind configurado e talvez você curta outra ferramenta para estilização.

É interessante apagar os dados do Mongo Atlas. Depois que o projeto estiver em produção, o ideal seria criar uma coleção apenas para ambiente de desenvolvimento e gerenciar os ambientes via variável de ambiente, bem tranquilo de fazer com NextJS & Vercel ❤️.

Interessante também seria integrar essa aplicação que está na vercel com o GitHub, assim cada commit na master (agora é main) gera a build automaticamente.

NextJS é fantástico, Vercel show d+ e SRW delicinha!

Com certeza depois desse post você deve ter ficado com vontade de estudar mais sobre o NextJS. Então vamos para os links abaixo.

E aí, o que achou do post?

Espero que tenha curtido! 💜

O aprendizado é contínuo e sempre haverá um próximo nível! 🚀