Newsletter para devsEntra

Cómo crear un blog con Nuxt, Content y Markdown

NuxtJS es una herramienta para crear aplicaciones web usando toda la potencia de Vue tanto en la aprte del cliente como en la del servidor.

Ha recibido inversiones millonarias y su ecosistema no para de crecer y mejorar.

Vemos en este tutorial una guía para empezar, en muy pocos pasos, un blog construido sobre ficheros Markdown.

Aquí puedes ver una demostración.

Y, al final del todo, los enlaces.

(Un secreto nada más empezar: también funciona para JSON, Yaml y CSV).

Eso si, hay tantas opciones posibles que en nuestros directos Live Coding hemos creado y desplegado una aplicación web completa con NuxtJS y Github Actions.

Muy pronto disponible en formato curso, apúntate para ser el primero en enterarte.

Instalación de Nuxt y el módulo Content

El proceso de instalación de Nuxt es tan sencillo como ejecutar este comando:

npx create-nuxt-app nuxt-content-headless-blog

Donde nuxt-content-headless-blog es el nombre de la carpeta donde se va a crear la app.

Debes tener instalado node en tu ordenador. La versión con la que he trabajado para esta aplicación es la 13.11.

Te hará varias preguntas en la instalación. Elige siempre las más sencillas.

Solo te hace falta una para que todo esto sea compatible con tu proyecto. Al preguntar por un framework CSS, elige Tailwind CSS.

Esto hará que tengas una configuración por defecto para cargar los ficheros de Tailwind.

Puedes arrancar la aplicación de demostración con:

cd nuxt-content-headless-blog
npm run dev

En la pantalla te aparecerá algo como esto y podrás acceder a tu entorno local de desarrollo.

NuxtJS Servidor Localhost

Nos queda un paso muy importante.

Instalar el módulo oficial Content, creado por el equipo de Nuxt. Una auténtica delicia.

npm install @nuxt/content

Tienes que agregar el registro del módulo en el archivo nuxt.config.js

{
  modules: [
    '@nuxt/content'
  ],
}

Ya lo tenemos todo listo.

Con esto acabas de dotar a Nuxt de la capacidad de generar una APi para convertir a tu aplicación en un Git-Headless CMS.

Crear el contenido en formato Markdown

Markdown es un formato popularizado por Github que nos permite crear documentos con un marcado específico para que, al ser procesados, generen HTML que pueda entender el navegador.

Nuestros ficheros markdown tienen una peculiaridad, ya que contienen en su cabecera una sección de configuración llamada frontmatter.

Un ejemplo para que veas:

---
title: Este es el título de tu contenido
description: Aquí una descripción
published: true
---

Esta estructura es libre, puedes crearla como quieras, aunque luego vas a tener que emplearla en el código.

Creamos los ficheros de contenido en la carpeta /content. Puede modificarse mediante configuración, pero el módulo Content buscará ahí por defecto.

Si te dijas en el repositorio en la carpeta /content tenemos este esquema de contenido, que es el que vamos a emplear en este manual.

content
├── index.md
└── posts
    ├── ipsum.md
    └── lorem.md

Crear nuestra primera página

Nuxt necesita algo de código para saber que tiene que cargar en la página principal de la web.

Creamos el fichero /pages/index.vue.

Esto va a decirle a Nuxt que estamos creando el contenido de la home que vas as poder ver en http://localhost:3000/

<template>
  <div>
    <h1 class='text-3xl py-6'>{{ index.title }}</h1>
    <p class='text-xl py-3'>{{ index.description }}</p>
    <nuxt-content :document='index' class='leading-loose' />
  </div>
</template>

<script>
export default {
  async asyncData({ $content, params, error }) {
    const index = await $content('index')
      .fetch()
      .catch(err => {
        error({ statusCode: 404, message: 'index not found' });
      });

    return {
      index
    };
  }
};
</script>


Aquí pasan muchas cosas:

  • Es la sintaxis que usamos en Vue, las páginas son componentes de VueJS pero con métodos hipervitaminados. Si no conoces Vue, tenemos un curso que puede venirte bien.
  • Comenzamos por <script>. El método asyncData es una de esas herramientas que nos provee Nuxt. Lo que pase dentro va a poder estar accesible en la template sin tener que definir expresamente la propiedad.
  • La potencia está en $content('index'). Esta llamada es capaz de capturar el fichero que creamos antes en /content/index.md y junto con fetch() tenerlo a nuestra disposición como un objeto con el que poder trabajar en JavaScript.
  • Que no se nos olvide el return {index} para tener accesible ese objeto en el template.

En la parte de la plantilla HTML puedes ver como usamos clases propias de la librería TailwindCSS, de la que hablamos en profundidad en un LiveCoding.

  • ¿Recuerdas lo que te hablé más arriba del frontmatter? Pues eso nos facilita usar las variables {{ index.title }} o {{ index.description }} en nuestra plantilla.
  • El nuxt-content es un componente propio del módulo Content al que hay que pasarle el documento que queremos procesar desde Markdown hacia HTML mediante el atributo document.

En este punto tenemos todas las piezas enganchadas.

Si accedes a la home de tu aplicación local, podrás ver el contenido de /content/index.md pintado de forma preciosa.

Una página para cada post

Un blog tiene que tener contenido. Cada artículo con su propio enlace.

Lo primero que te voy a contar es que la forma de estructurar las carpetas y nombres de Nuxt dentro de pages es muy generosa.

Si creas un fichero pages\carpeta\fichero.vue podrás acceder a la ruta /carpeta/fichero, porque Nuxt se encarga de crearla para ti.

Un paso más allá: las rutas dinámicas.

Para ellas Nuxt utiliza un nombre de parámetro con un prefijo en forma de barra baja _.

Así que vamos a crear el fichero pages/posts/_slug.vue.

Cada vez que accedemos a /posts/aqui-va-el-slug Nuxt intentará cargar su contenido en ese fichero recién creado, siendo slug el parámetro dinámico que intentará buscar en algún sitio de content un aqui-va-el-slug.md.

Vamos a por ello. Creamos pages/posts/_slug.vue.

<template>
  <div>
      <h1
        class='my-8 max-w-full m-auto text-3xl text-center font-medium'
      >
        {{ post.title }}
      </h1>
      <h3 class='py-4 text-center uppercase'>{{ post.description }}</h3>
      <nuxt-content :document='post' class='leading-loose' />
  </div>
</template>

<script>
export default {
  async asyncData({ $content, params, error }) {
    const post = await $content(`posts/${params.slug}`)
      .fetch()
      .catch(err => {
        error({ statusCode: 404, message: 'Página no encontrada' });
      });

    return {
      post
    };
  }
};
</script>

Es muy parecido al caso de index.vue pero con algunas particularidades.

  • En asyncData ahora tenemos una llamada a $content(posts/${params.slug}). En params.slug es donde tenemos ese alias de ruta que va a coincidir con el nombre de un archivo que estará en la carpeta /content/posts.
  • Si te das cuenta $content funciona como una API, intentando capturar los datos pasándole una ruta concreta. Prueba a cargar http://localhost:3000/_content/ en tu navegador y la verás en marcha, gracias al módulo Content. (Esto es lo que te contaba al principio de Git Headless CMS.)
  • Con fetch() convertimos el contenido en Markdown en algo que pueda entender JavaScript y lo almacenamos en post que estará disponible en nuestro template.
  • Si algo no funciona bien, capturamos el error y generamos una página de error 404.

El resto del código se parece mucho a lo que vimos en el paso anterior.

Listado de posts en la home

Todo buen blog necesita de un listado de contenidos a los que poder acceder.

Hagámoslo aprovechando lo que nos ofrece el método fetch de Nuxt.

Volvemos a \pages\index.vue y modificamos su contenido.

<template>
  <div>
    <h1 class='text-3xl py-6'>{{ index.title }}</h1>
    <p class='text-xl py-3'>{{ index.description }}</p>
    <nuxt-content :document='index' class='leading-loose' />
    <ul class='list-disc list-inside mb-4'>
      <li v-for='(post, index) in posts' :key='index'>
        <nuxt-link :to='post.path' class='underline'>{{ post.title }}</nuxt-link>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  async asyncData({ $content, params, error }) {
    const index = await $content('index')
      .fetch()
      .catch(err => {
        error({ statusCode: 404, message: 'index not found' });
      });

    const posts = await $content('posts')
      .only(['title', 'path'])
      .limit(5)
      .sortBy('title')
      .where({
        published: true
      })
      .fetch()
      .catch(err => {
        error({ statusCode: 404, message: 'Página no encontrada' });
      });

    return {
      index,
      posts
    };
  }
};
</script>

Han aparecido cosas nuevas.

  • Empezamos por script como siempre. Ahora tenemos un $content('posts') que tiene más modificadores de los que habíamos visto hasta ahora.

    • Solo queremos el title y el path y por eso usamos only(). El path lo genera Content por nosotros basándose en la ruta de los ficheros Markdown del blog.
    • Con limit(5) nos quedamos sólo con 5 resultados.
    • Los ordenamos alfabéticamente por el título con sortBy('title').
    • Incluso podemos hacer una query gracias al método where. Así solo mostraremos los posts que tengan el atributo published a true en su cabecera.
  • Con posts disponible en <template> solo resta hacer un bucle v-for tan habitual en VueJS para generar una lista de enlaces.

Si accedes a tu home en local tendrás algo parecido a esto.

En el repositorio tienes más estilos y un toque de HTML para que todo quede más bonito. Puedes encontrarlos en /layout/default.vue y /pages/posts/_slug.vue

Bola extra: Generar las rutas estáticas

Una de las características de Nuxt es que puedes crear aplicaciones web que se ejecuten en el servidor y en el cliente.

Lo que le hace especial y fácil de desplegar es su capacidad de generar todo el contenido de forma estática. Igual que Jekyll, Gatsby, Eleventy y tantos otros.

Si lanzas este comando:

npm run generate

Se creará una carpeta /dist con todo tu contenido en formato estático.

Puedes cargarlo en tu navegador directamente.

Pero, ¿qué pasa con los posts?

Aunque sean accesibles desde el listado, no están generados estáticamente. Solo dinámicamente. Si alguien accede por la ruta creada no será capaz de acceder.

Nuxt necesita que le digas donde están las rutas que tiene que generar.

Así que nos vamos a nuxt.config.js y añadimos esta configuración, justo debajo del modules que colocamos al comienzo de todo.

  generate: {
    async routes() {
      const { $content } = require('@nuxt/content');
      const files = await $content('posts')
        .only(['path'])
        .fetch();

      return files.map(file => (file.path === '/index' ? '/' : file.path));
    }
  },

Si vuelves a lanzar npm run generate verás como ahora si se generan las rutas de los posts que estaban pendientes.

¡Ya tienes tu blog preparado y listo!

Enlaces del tutorial

Si quieres seguir aprendiendo, puedes hacerlo en mi Curso gratis.

Escrito por:

Imagen de Daniel Primo

Daniel Primo

CEO en pantuflas de Web Reactiva. Programador y formador en tecnologías que cambian el mundo y a las personas. @delineas en twitter y canal @webreactiva en telegram

12 recursos para developers cada domingo en tu bandeja de entrada

Además de una skill práctica bien explicada, trucos para mejorar tu futuro profesional y una pizquita de humor útil para el resto de la semana. Gratis.