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.
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.
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.
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.
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.