Vamos a montar un blog estático utilizando Next.js y dev.to como headless CMS.
Si quieres ir directamente al resultado final en este repo tienes el proyecto final que también sirve como boilerplate para futuros blogs estáticos.
Motivación
Cuando estaba haciendo el blog para Nimbel necesitaba hacer un blog de forma rápida y que se adaptase a la naturaleza estática del resto de la página. Desde Nimbel queriamos poder publicar articulos en Dev.to y al mismo tiempo mantener actualizado el blog personal.
La estrategia que seguiremos en este tutorial será:
- Aprovechar las capacidades estáticas de NextJS y la API de Dev.to para hacer un fetch de los post del usuario en tiempo de build.
- Crear las rutas estáticas a todos los post que hemos hecho fetch.
- Utilizar los webhooks de Dev.to para que cada vez que el usuario cree y/o actualice un post, se genere un nuevo build de nuestro sitio estático.
- Crear una plantilla base (boileplate) que nos servirá para crear cualquier otro blog siguiendo esta misma estrategia.
Paso a paso
Pre requisitos
Creación del proyecto
En mi caso utilicé mi propio boilerplate de NextJS con TailwindCSS que podéis descargar desde aquí o simplemente utilizando uno de los siguientes comandos:
yarn create next-app my-app-name --example "https://github.com/dastasoft/nextjs-boilerplate"
npx create-next-app my-app-name --use-npm --example "https://github.com/dastasoft/nextjs-boilerplate"
Esto os creará un nuevo proyecto NextJS con TailwindCSS ya configurado.
Estructura
En NextJS no necesitamos definir rutas, cada JS que esté dentro de la carpeta pages
será considerado una ruta accesible (menos _app
y otros _
archivos que se consideran privados).
Organizaremos el proyecto con las siguientes rutas:
- pages
|- blog
|-- posts
|--- [slug].js
|- _app.js
|- blog.js
|- index.js
-
_app.js
contendrá el layout general de la aplicación que aplicaremos a todas las rutas de nuestra aplicación. -
blog.js
contendrá la estructura general de la página dedicada al blog así como el fetch a los posts para poder mostrarlos en forma de tarjetas. -
index.js
será nuestra pagina de inicio. -
blog/posts/[slug].js
este punto necesita algo mas de explicación:- Al crear una estructura le estamos diciendo al router que en la ruta
nuestro-dominio/blog/posts/slug
encontrará un elementoslug
que será dinámico y estará accesible mediante era ruta exacta. - Dentro de ese JS deberemos definir que valor toma el parametro dinámico
slug
, que en nuestro caso sera el propio slug (url) del post, por lo que deberemos hacer un fetch de ese post en concreto y consultar sus datos en tiempo de build. - Deberemos definir todos los paths posibles (uno por cada post) de cara a que cuando el usuario navegue o escriba directamente en la url
nuestro-dominio/blog/post/este-post-existe
ese slug ya este creado en tiempo de build, ya que la página es totalmente estática y no irá a consultar nuevos datos fuera del build*.
- Al crear una estructura le estamos diciendo al router que en la ruta
SSG vs SSR vs ISR
- SSG (Static Site Generation), es el modo por defecto en el que trabaja NextJS, se puede utilizar en combinación con las funciones
getStaticProps
ygetStaticPaths
que provee el propio framework, las diferentes páginas se generan de forma estática en tiempo de build. - SSR (Server Side Rendering), se generán las páginas bajo demanda por cada petición desde el servidor, se utiliza en combinación con la función
getServerSideProps
. - ISR (Incremental Static Regeneration), disponible a partir de la version 9.5 de NextJS. Te perimite actualizar páginas que se crearon como estáticas y al entrar una nueva petición se detecta que está en un estado obsoleto y debe re-renderizarse. Para activar ISR se añade una propiedad
revalidate
en la funcióngettaticProps
.
En esta guía vamos a tratar solo SSG, para información mas detallada de los otros metódos consultar la documentación oficial, NextJS no necesita ninguna configuración especial para cambiar (o incluso combinar!) entre los diferentes modos, todo recae en la utilización de las funciones especiales ligadas a cada tipo.
Este es un apartado complejo y muy amplio y es precisamente donde NextJS brilla por la posibilidad de elegir fácilmente entre ellos o incluso combinarlos. Lo dejo para una futura guía :) la cual deberia explicar cuando utilizar unos metódos u otros segun la naturaleza de cada página.
En nuestro caso, debido a que todos los datos los tenemos disponibles en tiempo de build, dado que los vamos a buscar a la API de dev.to y no tenemos que cambiar nada de nuestra web a menos que cambie algo en nuestro CMS (dev.to) no tiene sentido estar repitiendo las mismas consultas por cada usuario que entra.
Variables de entorno
A lo largo de las siguientes secciones utilizaremos una variable de entorno para poder acceder al usuario de dev.to y poder descargarnos los articulos publicados. De cara al desarrollo en local utilizaremos el fichero .env.development
en el que añadiremos la siguiente variable de entorno:
DEV_USERNAME=dastasoft
Si utilizáis directamente el boilerplate solo tenéis que cambiar el valor de esta variable para que consulte vuestro usuario en vez del mio.
Esta variable de entorno la necesitaremos configurar también en el momento del despliegue, en este tutorial desplegaremos la aplicación utilizando Vercel por lo que podéis consultar la seccion de Despliegue
.
Creando el Blog
Empezaremos creando el blog.js
en nuestra carpeta pages
.
La parte mas importante es como hacemos fetch de todos los posts de un usuario en tiempo de build para poder pintar los posts como tarjetas, para ello utilizaremos una de las funciones SSG que nos proporciona NextJS, getStaticProps
:
export const getStaticProps = async () => {
const devDotToPosts = await fetch(
`https://dev.to/api/articles?username=${process.env.DEV_USERNAME}`
);
const res = await devDotToPosts.json();
return {
props: {
devDotToPosts: res
}
};
};
Creando el Artículo
El siguiente paso a realizar para que la generación estática sea posible es definir todas las posibles rutas que el usuario pueda visitar al entrar en esta página, para que sean accesibles las tenemos que pre-renderizar en tiempo de build y NextJS necesita saber la lista completa, esto lo conseguiremos con otra de las funciones que provee NextJS getStaticPaths
.
export async function getStaticPaths() {
const devDotToPosts = await fetch(
`https://dev.to/api/articles?username=${process.env.DEV_USERNAME}`
);
const posts = await devDotToPosts.json();
return {
paths: posts.map(post => {
return {
params: {
slug: post.slug
}
};
}),
fallback: false
};
}
Creamos una ruta por cada post publicado, utilizando su slug
como en el caso anterior. Definimos fallback
como false
ya que no planeamos soportar URLs que esten fuera de las que estamos generando estáticamente, tener esta propiedad a false devolverá un 404 si se intenta consultar cualquier URL que este fuera del array que proporcionamos en paths
.
Habilitar la propiedad fallback
tiene numerosas aplicaciones y puede ser utilizada en combinación con Incremental Static Generation
el cual es una opción muy potente dentro de NextJS, para más información sobre este tema consultar la documentación oficial
Datos del artículo
Dentro del artículo en concreto, necesitamos recuperar los datos, para ello consultaremos la API de dev.to usando el mismo slug
con el que hemos construido la URL.
export const getStaticProps = async ({ params }) => {
const devDotToPost = await fetch(
`https://dev.to/api/articles/${process.env.DEV_USERNAME}/${params.slug}`
);
const res = await devDotToPost.json();
return {
props: {
devDotToPost: res
}
};
};
Todos los datos que nos llegan desde la API de dev.to los pasamos en tiempo de build a la página del artículo en concreto, estos datos serán accesibles a través de la prop
devDotToPost
.
export default function Post({ devDotToPost }) {
...
}
Imprimir el markdown
Una vez ya tenemos los datos del artículo, entre los múltiples campos que nos llegan de la API, el contenido en markdown está en body_html
, para utilizarlo:
<div className="markdown" dangerouslySetInnerHTML={{ __html: body_html }} />
En la clase markdown
deberás definir como quieres que se vean los elementos contenidos en el markdown ya que la API devuelve una versión raw del markdown. En el proyecto de ejemplo tienes disponible una propuesta simple.
[slug].js al completo
Así es como queda nuestra template para cualquier artículo, podéis verlo directamente en el repo:
import Head from 'next/head';
import Link from 'next/link';
import TopButton from '../../../components/TopButton';
export default function Post({ devDotToPost }) {
const {
title,
published_at,
social_image,
body_html,
user,
type_of,
description,
canonical_url
} = devDotToPost;
const date = new Date(published_at);
const formatedDate = `${date.getDate()}/${
parseInt(date.getMonth(), 10) + 1
}/${date.getFullYear()}`;
return (
<div>
<Head>
<meta property="og:type" content={type_of} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={social_image} />
<meta property="og:url" content={canonical_url} />
</Head>
<div className="flex justify-center">
<TopButton />
<article className="text-xs w-full md:w-3/4 ">
<div className="border-2 text-black bg-white md:rounded-lg overflow-hidden">
<img className="w-full" src={social_image} alt={title} />
<div className="p-4 md:p-32">
<h1>{title}</h1>
<div className="flex items-center text-gray-600">
<img
className="rounded-full w-12"
src={user.profile_image_90}
alt={user.name}
/>
<span className="mx-4">{user.name}</span>
<span className="text-sm">{formatedDate}</span>
</div>
<div
className="markdown"
dangerouslySetInnerHTML={{ __html: body_html }}
/>
</div>
</div>
<Link href="/blog">
<a className="text-blue-500 inline-flex items-center md:mb-2 lg:mb-0 cursor-pointer text-base pb-8">
<svg
className="w-4 h-4 mr-2"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
viewBox="0 0 24 24"
>
<path d="M19 12H5M12 19l-7-7 7-7" />
</svg>
Back
</a>
</Link>
</article>
</div>
</div>
);
}
export const getStaticProps = async ({ params }) => {
const devDotToPost = await fetch(
`https://dev.to/api/articles/${process.env.DEV_USERNAME}/${params.slug}`
);
const res = await devDotToPost.json();
return {
props: {
devDotToPost: res
}
};
};
export async function getStaticPaths() {
const devDotToPosts = await fetch(
`https://dev.to/api/articles?username=${process.env.DEV_USERNAME}`
);
const posts = await devDotToPosts.json();
return {
paths: posts.map(post => {
return {
params: {
slug: post.slug
}
};
}),
fallback: false
};
}
Layout
Para crear el layout y que aplique a todas las pantallas, lo crearemos en el fichero _app.js
e internamente NextJS lo añadirá a todas las paginas:
import Link from 'next/link';
import '../styles/index.css';
export default function App({ Component, pageProps }) {
return (
<div>
<nav className="p-4 flex justify-center items-center mb-4" id="nav">
<Link href="/">
<span className="text-xl font-bold cursor-pointer mr-4">Home</span>
</Link>
<Link href="/blog">
<span className="text-xl font-bold cursor-pointer">Blog</span>
</Link>
</nav>
<main className="container px-5 mx-auto">
<Component {...pageProps} />
</main>
</div>
);
}
Lo importante en este punto es:
- Utilizar el componente
Link
de NextJS para que la navegación sea correcta - Es el sitio ideal para importar el archivo de css y que aplique de forma global.
- Asegurarse de tener
<Component {...pageProps} />
ya que sin esto no veremos los componentes hijos, (similar a la utilizacion dechildren
en React)
Home
Definir la pagina principal en NextJS es tan sencillo como crear el fichero index.js
dentro de la carpeta pages
y NextJS creará automáticamente una ruta, en este caso a /
, la cual mezclará lo que hayamos definido en el fichero _app.js
mas el propio index.js
.
Esta es la propuesta de home page para el proyecto:
import DevDotToLogo from '../public/devdotto.svg';
import NextLogo from '../public/nextjs.svg';
export default function Home() {
return (
<div>
<div className="flex justify-center items-center">
<a
href="https://nextjs.org/"
target="_blank"
rel="noopener noreferrer"
aria-label="NextJS"
>
<NextLogo className="mr-4" width="100px" height="100px" />
</a>
<span className="text-2xl">Blog Boilerplate</span>
</div>
<div className="flex justify-center items-center">
<span className="text-2xl">with</span>
<a
href="https://dev.to/"
target="_blank"
rel="noopener noreferrer"
aria-label="Dev.to"
>
<DevDotToLogo className="mx-4" width="100px" height="100px" />
</a>
<span className="text-2xl">as a CMS</span>
</div>
</div>
);
}
En este caso se utilizan anchor
normales ya que son enlaces al exterior y NextJS no tiene que acceder a ningua ruta interna.
CSS
NextJS mostrará errores si intentáis introducir CSS que puedan afectar de forma global fuera del archivo _app.js
, por ello en los demas sitios como páginas y/o componentes es recomendable utilizar soluciones como emotionjs
, styled-components
, css-modules
o tailwindcss
como en esta guía, que tienen su rango de efecto limitado al propio componente.
NextJS provee su propia solución CSS-in-JS
llamada styled-jsx
pero últimamente de los propios proyectos quick-start de NextJS se ha optado por implementar css-modules
.
Si quereis conocer mejor que opciones tenéis para temas de estilo podéis consultar mi guia de estilos en React la cual aplica en su mayoria para NextJS, la diferencia principal es que no podemos aplicar estilos globales como hemos comentado anteriormente.
Despliegue
Desplegaremos este proyecto en la plataforma de los mismos creadores de NextJS que es Vercel. Para desplegar un proyecto en Vercel debéis seguir los siguientes pasos:
- Crear una cuenta en Vercel
- Pulsar en
Import Project
- Importaremos el proyecto directamente de nuestro repositorio Git
- Proporcionar la URL del repositorio GIT.
- En caso de que el paso anterior os de el error:
Couldn’t find the Git repository. If it exists, verify that the GitHub Integration is permitted to access it in the GitHub App Settings.
pulsar enGitHub App Settings
y añadir el repositorio que intentáis desplegar a la lista de accesos de Vercel, si es el primer despliegue que realizais os pedirá acceso como parte del proceso. - Una vez Vercel tenga visibilidad sobre el repositorio Git podremos darle un nombre, que puede ser cualquiera no tiene porque coincidir con git, un
Framework preset
que dejaremos tal y como esta marcado en Next.js,Build and Output Settings
que por el momento no necesitaremos cambiar nada y por últimoEnvironment Variables
aqui tendremos que crear la variable de entorno que definimos anteriormente en.env.development
- Dentro de
Environment Variables
definimos la variableDEV_USERNAME
con el valor del usuario sobre el que queráis hacer las consultas, en mi casodastasoft
y pulsamosAdd
- Pulsamos
Deploy
Es posible que la primera vez el despliegue falle dando errores de recibir respuestas JSON erroneas, en mi caso intentando el despliegue una segunda vez me funcionó sin problemas.
Podéis ver el resultado final desplegando el boilerplate que hemos construido en este tutorial en [https://dev-cms-static-blog.vercel.app/(https://dev-cms-static-blog.vercel.app/)
Actualización automática
Ya casi estamos, pero nos falta el paso más importante, ahora mismo tenemos un blog que se genera en tiempo de build de forma estática, eso quiere decir que cuando el proyecto se despliega en Vercel, se lanzan todas las consultas necesarias a dev.to para obtener la información necesaria y con eso se construye una web totalmente estática en la que por muchas visitas que tengamos, no se vuelve a consultar a dev.to para recuperar artículos.
Pero y si publicamos/editamos un artículo? Necesitamos una forma de decirle a Vercel que debe volver a pasar esa fase de build y recuperar la información más actualizada, para ello utilizaremos webhooks.
Crear una URL de acceso al despliegue
Dentro del proyecto en Vercel, debemos ir a Settings
a la sección referente a Git
y buscar el cuadro Deploy Hooks
, aquí crearemos un nuevo hook al cual le podemos dar el nombre que queramos y que este en nuestra rama principal de git, en mi caso:
- Nombre: dev.to
- Git Branch Name: master
Esto nos generará una URL del tipo https://api.vercel.com/v1/integrations/deploy/xxxxxxxxxxxxxxxxxxx
Crear webhooks en dev.to
En el README.md
del boilerplate teneis los comandos disponibles para consultar, crear y eliminar webhooks en vuestra cuenta de dev.to.
Necesitaréis acceso a una Terminal y el paquete curl, además en vuestra cuenta de dev.to necesitaréis crear una DEV API Key
, esto lo podéis hacer accediendo a dev.to con vuestra cuenta en el apartado Settings
, Account
y en la sección DEV API Keys
.
Para crear la DEV API Key hay que proporcionar un nombre y pulsar en Generate API Key
, esto nos generará un hash que necesitaremos en los siguientes comandos.
Con una terminal abierta utilizamos el siguiente comando para crear el webhook en nuestra cuenta de dev.to
curl -X POST -H "Content-Type: application/json" \
-H "api-key: API_KEY" \
-d '{"webhook_endpoint":{"target_url":"TARGET_URL","source":"DEV","events":["article_created", "article_updated"]}}' \
https://dev.to/api/webhooks
Donde API_KEY
es la DEV API Key que hemos creado en dev.to y TARGET_URL
(importante mantener las ") es la URL de acceso al despliegue que hemos creado en Deploy Hooks
de Vercel. En este ejemplo estamos poniendo a la escucha el webhook para los eventos de creación de artículos y también para la edición, podéis dejar los eventos que os interesen.
Comprobar webhook
En una terminal con curl disponible ejecutar el siguiente comando:
curl -H "api-key: API_KEY" https://dev.to/api/webhooks
Donde API_KEY
es la DEV API Key que hemos creado en dev.to.
Debe respondernos con un array el cual no debe estar vacío ya que en el paso anterior creamos un webhook. Si os sale como respuesta un array vacío, comprobad el paso anterior.
Conclusión
Si se ha creado satisfactoriamente el webhook, lo que habremos conseguido es que cada vez que un artículo se cree o se edite (segun los eventos que hayais utilizado) llamará a la URL que le hemos porporcionado, esta URL desencadenará un nuevo build en Vercel que volverá a consultar la API de dev.to y encontrará el nuevo artículo generando de nuevo una versión totalmente estática de nuestro blog.
Con esto ya tendríamos completados los requisitos que nos habiamos fijado al principio de este tutorial! Os animo a indagar más en el proyecto boilerplate sobre el que esta basado este tutorial para que lo podáis utilizar como base para futuros proyectos.
Ahora es vuestro turno, cual es vuestra experiencia creando blogs? Crees que es más sencillo de la forma que lo haces actualmente o con esta forma? Ya utilizabas esta forma o una parecida, cuentame tu caso de éxito o tus preguntas :D
Con un poco de suerte, este post creará una nueva entrada en el Blog de Nimbel
Disfrutad!
Top comments (2)
Me gusta mucho la idea de sincronizar los post con dev.to, ya me guarde el post para verlo con más detenimiento y probar alguna que otra cosa 🙂, muy buen aporte y gracias por compartirlo !
Some comments may only be visible to logged-in visitors. Sign in to view all comments.