Creación de un blog con Next.js | Programar Plus

En este artículo, usaremos Next.js para construir un marco de blog estático con el diseño y la estructura inspirados en Jekyll. Siempre he sido un gran admirador de cómo Jekyll hace que sea más fácil para los principiantes configurar un blog y, al mismo tiempo, también proporciona un gran grado de control sobre todos los aspectos del blog para los usuarios avanzados.

Con la introducción de Next.js en los últimos años, combinada con la popularidad de React, existe una nueva vía para explorar para blogs estáticos. Next.js hace que sea muy fácil crear sitios web estáticos basados ​​en el propio sistema de archivos con poca o ninguna configuración requerida.

La estructura de directorios de un blog típico de Jekyll básico se ve así:

.
├─── _posts/          ...blog posts in markdown
├─── _layouts/        ...layouts for different pages
├─── _includes/       ...re-usable components
├─── index.md         ...homepage
└─── config.yml       ...blog config

La idea es diseñar nuestro marco alrededor de esta estructura de directorios tanto como sea posible para que sea más fácil migrar un blog desde Jekyll simplemente reutilizando las publicaciones y configuraciones definidas en el blog.

Para aquellos que no estén familiarizados con Jekyll, es un generador de sitios estáticos que puede transformar su texto sin formato en sitios web y blogs estáticos. Consulte la guía de inicio rápido para comenzar a trabajar con Jekyll.

Este artículo también asume que tienes un conocimiento básico de React. Si no es así, la página de inicio de React es un buen lugar para comenzar.

Instalación

Next.js funciona con React y está escrito en Node.js. Entonces, primero debemos instalar npm, antes de agregar next, react y react-dom al proyecto.

mkdir nextjs-blog && cd $_
npm init -y
npm install next react react-dom --save

Para ejecutar los scripts de Next.js en la línea de comando, tenemos que agregar el next comando al scripts sección de nuestro package.json.

"scripts": {
  "dev": "next"
}

Ahora podemos correr npm run dev en la línea de comando por primera vez. Veamos qué pasa.

$ npm run dev
> [email protected] dev /~user/nextjs-blog
> next

ready - started server on http://localhost:3000
Error: > Couldn't find a `pages` directory. Please create one under the project root

El compilador se queja de que falta un directorio de páginas en la raíz del proyecto. Aprenderemos sobre el concepto de páginas en la siguiente sección.

Concepto de páginas

Next.js se basa en el concepto de páginas. Cada página es un componente de React que puede ser de tipo .js o .jsx que se asigna a una ruta basada en el nombre del archivo. Por ejemplo:

File                            Route
----                            -----
/pages/about.js                 /about
/pages/projects/work1.js        /projects/work1
/pages/index.js                 /

Vamos a crear el pages directorio en la raíz del proyecto y llenar nuestra primera página, index.js, con un componente básico de React.

// pages/index.js
export default function Blog() {
  return <div>Welcome to the Next.js blog</div>
}

Correr npm run dev una vez más para iniciar el servidor y navegar a http://localhost:3000 en el navegador para ver su blog por primera vez.

Captura de pantalla de la página de inicio en el navegador.  El contenido dice bienvenido al blog next.js.

Fuera de la caja, obtenemos:

  • Recarga en caliente para que no tengamos que actualizar el navegador para cada cambio de código.
  • Generación estática de todas las páginas dentro del /pages/** directorio.
  • Archivo estático que sirve para activos que viven en el/public/** directorio.
  • Página de error 404.

Navegue a una ruta aleatoria en localhost para ver la página 404 en acción. Si necesita una página 404 personalizada, los documentos de Next.js tienen excelente información.

Captura de pantalla de la página 404.  Dice 404 No se pudo encontrar esta página.

Páginas dinámicas

Las páginas con rutas estáticas son útiles para construir la página de inicio, acerca de la página, etc. Sin embargo, para construir dinámicamente todas nuestras publicaciones, usaremos la capacidad de ruta dinámica de Next.js. Por ejemplo:

File                        Route
----                        -----
/pages/posts/[slug].js      /posts/1
                            /posts/abc
                            /posts/hello-world

Cualquier ruta, como /posts/1, /posts/abc, etc., se corresponderá con /posts/[slug].js y el parámetro slug se enviará como parámetro de consulta a la página. Esto es especialmente útil para las publicaciones de nuestro blog porque no queremos crear un archivo por publicación; en su lugar, podríamos pasar dinámicamente el slug para renderizar la publicación correspondiente.

Anatomía de un blog

Ahora, dado que entendemos los bloques de construcción básicos de Next.js, definamos la anatomía de nuestro blog.

.
├─ api
│  └─ index.js             # fetch posts, load configs, parse .md files etc
├─ _includes
│  ├─ footer.js            # footer component
│  └─ header.js            # header component
├─ _layouts
│  ├─ default.js           # default layout for static pages like index, about
│  └─ post.js              # post layout inherts from the default layout
├─ pages
│  ├─ index.js             # homepage
|  └─ posts                # posts will be available on the route /posts/
|     └─ [slug].js       # dynamic page to build posts
└─ _posts
   ├─ welcome-to-nextjs.md
   └─ style-guide-101.md

API de blog

Un marco de blog básico necesita dos funciones de API:

  • Una función para recuperar los metadatos de todas las publicaciones en _posts directorio
  • Una función para buscar una sola publicación para una determinada slug con el HTML y los metadatos completos

Opcionalmente, también nos gustaría que toda la configuración del sitio esté definida en config.yml estar disponible en todos los componentes. Entonces necesitamos una función que analice la configuración de YAML en un objeto nativo.

Dado que estaríamos tratando con muchos archivos que no son JavaScript, como Markdown (.md), YAML (.yml), etc, usaremos el raw-loader library para cargar archivos como cadenas para facilitar su procesamiento.

npm install raw-loader --save-dev

A continuación, debemos decirle a Next.js que use raw-loader cuando importamos formatos de archivo .md y .yml creando un next.config.js archivo en la raíz del proyecto (más información sobre eso).

module.exports = {
  target: 'serverless',
  webpack: function (config) {
    config.module.rules.push({test:  /.md$/, use: 'raw-loader'})
    config.module.rules.push({test: /.yml$/, use: 'raw-loader'})
    return config
  }
}

Next.js 9.4 introdujo alias para importaciones relativas que ayudan a limpiar los espaguetis de declaraciones de importación causados ​​por rutas relativas. Para usar alias, cree un jsconfig.json archivo en el directorio raíz del proyecto especificando la ruta base y todos los alias de módulo necesarios para el proyecto.

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@includes/*": ["_includes/*"],
      "@layouts/*": ["_layouts/*"],
      "@posts/*": ["_posts/*"],
      "@api": ["api/index"],
    }
  }
}

Por ejemplo, esto nos permite importar nuestros diseños con solo usar:

import DefaultLayout from '@layouts/default'

Obtener todas las publicaciones

Esta función leerá todos los archivos de Markdown en el _posts directorio, analice el contenido inicial definido al principio de la publicación utilizando materia gris y devuelva la matriz de metadatos para todas las publicaciones.

// api/index.js
import matter from 'gray-matter'


export async function getAllPosts() {
  const context = require.context('../_posts', false, /.md$/)
  const posts = []
  for(const key of context.keys()){
    const post = key.slice(2);
    const content = await import(`../_posts/${post}`);
    const meta = matter(content.default)
    posts.push({
      slug: post.replace('.md',''),
      title: meta.data.title
    })
  }
  return posts;
}

Una publicación típica de Markdown se ve así:

---
title:  "Welcome to Next.js blog!"
---
**Hello world**, this is my first Next.js blog post and it is written in Markdown.
I hope you like it!

La sección descrita por --- se llama la materia principal que contiene los metadatos de la publicación como, título, enlace permanente, etiquetas, etc. Aquí está el resultado:

[
  { slug: 'style-guide-101', title: 'Style Guide 101' },
  { slug: 'welcome-to-nextjs', title: 'Welcome to Next.js blog!' }
]

Asegúrese de instalar la biblioteca de materia gris de npm primero usando el comando npm install gray-matter --save-dev.

Obtener una sola publicación

Para un slug dado, esta función localizará el archivo en el _posts directorio, analice Markdown con la biblioteca marcada y devuelva el HTML de salida con metadatos.

// api/index.js
import matter from 'gray-matter'
import marked from 'marked'


export async function getPostBySlug(slug) {
  const fileContent = await import(`../_posts/${slug}.md`)
  const meta = matter(fileContent.default)
  const content = marked(meta.content)    
  return {
    title: meta.data.title, 
    content: content
  }
}

Salida de muestra:

{
  title: 'Style Guide 101',
  content: '<p>Incididunt cupidatat eiusmod ...</p>'
}

Asegúrese de instalar la biblioteca marcada desde npm primero usando el comando npm install marked --save-dev.

Config

Para reutilizar la configuración de Jekyll para nuestro blog Next.js, analizaremos el archivo YAML usando el js-yaml library y exporte esta configuración para que pueda usarse en todos los componentes.

// config.yml
title: "Next.js blog"
description: "This blog is powered by Next.js"


// api/index.js
import yaml from 'js-yaml'
export async function getConfig() {
  const config = await import(`../config.yml`)
  return yaml.safeLoad(config.default)
}

Asegúrate de instalar js-yaml desde npm primero usando el comando npm install js-yaml --save-dev.

Incluye

Nuestra _includes El directorio contiene dos componentes básicos de React, <Header> y <Footer>, que se utilizará en los diferentes componentes de diseño definidos en el _layouts directorio.

// _includes/header.js
export default function Header() {
  return <header><p>Blog | Powered by Next.js</p></header>
}


// _includes/footer.js
export default function Footer() {
  return <footer><p>©2020 | Footer</p></footer>
}

Diseños

Tenemos dos componentes de diseño en el _layouts directorio. Uno es el <DefaultLayout> que es el diseño base sobre el cual se construirán todos los demás componentes del diseño.

// _layouts/default.js
import Head from 'next/head'
import Header from '@includes/header'
import Footer from '@includes/footer'


export default function DefaultLayout(props) {
  return (
    <main>
      <Head>
        <title>{props.title}</title>
        <meta name="description" content={props.description}/>
      </Head>
      <Header/>
      {props.children}
      <Footer/>
    </main>
  )
}

El segundo diseño es el <PostLayout> componente que anulará el título definido en el <DefaultLayout> con el título de la publicación y renderice el HTML de la publicación. También incluye un enlace a la página de inicio.

// _layouts/post.js
import DefaultLayout from '@layouts/default'
import Head from 'next/head'
import Link from 'next/link'


export default function PostLayout(props) {
  return (
    <DefaultLayout>
      <Head>
        <title>{props.title}</title>
      </Head>
      <article>
        <h1>{props.title}</h1>
        <div dangerouslySetInnerHTML={{__html:props.content}}/>
        <div><Link href="https://css-tricks.com/"><a>Home</a></Link></div> 
      </article>
    </DefaultLayout>
  )
}

next/head es un componente incorporado para agregar elementos a la <head> de la página. next/link es un componente integrado que maneja las transiciones del lado del cliente entre las rutas definidas en el directorio de páginas.

Página principal

Como parte de la página de índice, también conocida como página de inicio, enumeraremos todas las publicaciones dentro del _posts directorio. La lista contendrá el título de la publicación y el enlace permanente a la página de la publicación individual. La página de índice utilizará el <DefaultLayout> e importaremos la configuración en la página de inicio para pasar el title y description al diseño.

// pages/index.js
import DefaultLayout from '@layouts/default'
import Link from 'next/link'
import { getConfig, getAllPosts } from '@api'


export default function Blog(props) {
  return (
    <DefaultLayout title={props.title} description={props.description}>
      <p>List of posts:</p>
      <ul>
        {props.posts.map(function(post, idx) {
          return (
            <li key={idx}>
              <Link href={'/posts/'+post.slug}>
                <a>{post.title}</a>
              </Link>
            </li>
          )
        })}
      </ul>
    </DefaultLayout>
  )
} 


export async function getStaticProps() {
  const config = await getConfig()
  const allPosts = await getAllPosts()
  return {
    props: {
      posts: allPosts,
      title: config.title,
      description: config.description
    }
  }
}

getStaticProps se llama en el momento de la compilación para pre-renderizar las páginas pasando props al componente predeterminado de la página. Usamos esta función para obtener la lista de todas las publicaciones en el momento de la compilación y mostrar el archivo de publicaciones en la página de inicio.

Captura de pantalla de la página de inicio que muestra el título de la página, una lista con dos títulos de publicaciones y el pie de página.

Página de publicación

Esta página mostrará el título y el contenido de la publicación para el slug suministrado como parte del context. La página de la publicación utilizará el <PostLayout> componente.

// pages/posts/[slug].js
import PostLayout from '@layouts/post'
import { getPostBySlug, getAllPosts } from "@api"


export default function Post(props) {
  return <PostLayout title={props.title} content={props.content}/>
}


export async function getStaticProps(context) {
  return {
    props: await getPostBySlug(context.params.slug)
  }
}


export async function getStaticPaths() {
  let paths = await getAllPosts()
  paths = paths.map(post => ({
    params: { slug:post.slug }
  }));
  return {
    paths: paths,
    fallback: false
  }
}

Si una página tiene rutas dinámicas, Next.js necesita conocer todas las rutas posibles en el momento de la compilación. getStaticPaths proporciona la lista de rutas que se deben representar en HTML en el momento de la compilación. La propiedad de reserva asegura que si visita una ruta que no existe en la lista de rutas, devolverá una página 404.

Captura de pantalla de la página del blog que muestra un encabezado de bienvenida y un saludo azul sobre el pie de página.

Listo para producción

Agregue los siguientes comandos para build y start en package.json, bajo la scripts sección y luego ejecutar npm run build seguido por npm run start para construir el blog estático e iniciar el servidor de producción.

// package.json
"scripts": {
  "dev": "next",
  "build": "next build",
  "start": "next start"
}

El código fuente completo de este artículo está disponible en este repositorio de GitHub. Siéntase libre de clonarlo localmente y jugar con él. El repositorio también incluye algunos marcadores de posición básicos para aplicar CSS a su blog.

Mejoras

El blog, aunque funcional, es quizás demasiado básico para la mayoría de los casos promedio. Sería bueno extender el marco o enviar un parche para incluir algunas características más como:

  • Paginación
  • Resaltado de sintaxis
  • Categorías y etiquetas para publicaciones
  • Estilismo

En general, Next.js parece realmente muy prometedor para crear sitios web estáticos, como un blog. Combinado con su capacidad para exportar HTML estático, podemos crear una aplicación verdaderamente independiente sin la necesidad de un servidor.