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:
- https://github.com/vercel/next.js/tree/canary/examples/with-mongodb
- https://github.com/vercel/next.js/tree/canary/examples/blog-starter-typescript
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:
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)
E também precisa de um usuário e senha para se conectar ao banco de dados:
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!
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?
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.
🔗 Links
- Variáveis de ambiente no NodeJS.
- Como inicio meus apps com ReactJS? Next.js, TypeScript, ESLint e Styled Components
- Essa feature fez o Next.js ser o framework mais popular
- Consumindo APIs no React com SWR
E aí, o que achou do post?
Espero que tenha curtido! 💜
O aprendizado é contínuo e sempre haverá um próximo nível! 🚀