Menu
Foto por: Thought Catalog

Cómo montamos este blog
Nuxt y markdown

Author image

Escrito por

Álex Martín

Cuando en Codetakers decidimos que íbamos a escribir un blog se nos pasaron principalmente dos ideas por la cabeza. La primera de ellas era usar Medium, ya que es una plataforma muy usada por desarrolladores, la segunda, usar WordPress. Realmente no llegaban a convencernos al 100% ninguna de ellas.

Por una parte, estaba Medium que hace unos meses cambió a un modelo de negocio que perjudicaba directamente al lector estableciendo un tiempo máximo de lectura antes de pasar por caja.

Por el otro lado, estaba WordPress, la plataforma de facto para blogs. Montar un WordPress nos daba pereza. No es un secreto que no nos guste demasiado y además queríamos algo más sencillo (o quizás no tanto).

A todo esto recordamos un maravilloso post de Marina donde explicaba cómo había desarrollado su blog y fue entonces cuando decidimos basar nuestro blog en las mismas tecnologías descritas por ella. Vue / Nuxt, NodeJS y Markdown eran herramientas con las que nos sentiríamos a gusto, así que empezamos a montar todo.

Estructura base

La web principal de Codetakers está desarrollada con Nuxt, por lo que crear nuevas páginas no sería más que añadir nuevas pages al esqueleto principal.

En nuestro caso creamos dos nuevas:

pages
    └── blog
        ├── _slug.vue
        └── index.vue

Si no conoces nada de Nuxt debes saber que el framework provee de una forma bastante sencilla de crear rutas dinámicas. En la estructura anterior, lo que permitimos es tener rutas para /blog donde mostraremos el índice con todos los posts y /blog/_slug donde _slug nos permite tener una ruta dinámica (blog/lo-que-sea)

Para el contenido crearemos la siguiente estructura donde alojamos los ficheros .md

contents
    └── blog
        ├── nombre-del-post-1.md
        └── nombre-del-post-2.md

Usando markdown

Para la extracción de contenidos usamos el loader de Webpack Fronmatter-loader que nos permite extraer el markdown con los atributos Front-matter.

Para configurar Frontmatter en Nuxt simplemente debemos añadirlo en el apartado build de nuxt.config.js

build: {
        extend(config, ctx) {
            config.module.rules.push({
                test: /\.md$/,
                loader: 'frontmatter-markdown-loader'
            })
        }
    },

De esta forma, cualquier fichero con extensión md será procesado por el loader.

Usar ahora un fichero en formato markdown simplemente quedaría relegado a importarlo en JavaScript. Como queremos que estos datos estén disponibles en SSR (Server side Rendering) realizamos la importación dentro del método asyncData que nos provee Nuxt para estos casos

async asyncData ({ params, error }) {
      const slug = params.slug
      try {
        const mdData = await import(`@/contents/blog/${slug}.md`)
        return {
	      ...
        }
      } catch (e) {
        error({ statusCode: 404, message: 'Artículo no encontrado' })
      }
}

De esta manera buscaremos en contents un fichero markdown que coincida con la url en la que se encuentra el post. En caso de no encontrarse devolveremos la página 404 previamente configurada en nuestro proyecto.

En los ficheros markdown tenemos una estructura similar a esta:

---
title: Título del post
image: /images/blog/mi-imagen.jpg
....
---

Texto de nuestro artículo

De manera que el objeto mdData incluirá lo siguiente:

mdData.attributes.title // Titulo del post
mdData.attributes.image // /images/blog/mi-imagen.jpg
mdData.html // Texto de nuestro 

Dando formato al código

Escribir código es algo que será muy común en este blog, por ello decidimos incluir la dependencia highlightjs, la cual nos “pone el código bonito” y además lo hace muy bien.

Para usarla dentro de nuestros componentes decidimos crearnos una directiva que haríamos reutilizable para todo el proyecto mediante el uso de plugins de Nuxt.

import Vue from "vue";
import hljs from 'highlight.js/lib/highlight'
import javascript from 'highlight.js/lib/languages/javascript'
import css from 'highlight.js/lib/languages/css'
import xml from 'highlight.js/lib/languages/xml'
import php from 'highlight.js/lib/languages/php'
import yaml from 'highlight.js/lib/languages/yaml'

hljs.registerLanguage('javascript', javascript)
hljs.registerLanguage('css', css)
hljs.registerLanguage('xml', xml)
hljs.registerLanguage('php', php)
hljs.registerLanguage('yaml', yaml)

import 'highlight.js/styles/zenburn.css'

Vue.directive('highlight', {
  inserted: function (el, binding, vnode) {
    let blocks = [...el.getElementsByTagName("code")]
    blocks.forEach(target => hljs.highlightBlock(target))
  }
})

Como al pasar de markdown a HTML nos incluye todo el código entre una etiqueta "code" lo que hacemos es recorrer cada una de éstas y aplicarle formato

Una vez configurado nuestro plugin dentro de nuxt.config.js, solo nos queda usarla en nuestra vista de la siguiente forma:

<div class="col-full" v-html="mdData.html" v-highlight></div>

Listado de posts

En nuestro blog no usamos una base de datos, por lo que para listar los posts tendríamos que añadirlos manualmente, y claro, somos programadores y eso no nos gusta un pelo.

Por esto, lo que ideamos es construir un pequeño servicio que nos listara todos los posts contenidos en la carpeta contents/blog. Evidentemente necesitamos algo de programación en el lado de servidor, por lo que decidimos no salirnos de nuestro stack y seguir con JavaScript, por lo que creamos un serverMiddleware que configuramos en nuestro nuxt.config.js

 serverMiddleware: [
        { path: '/api/posts', handler: '~/api/posts.js' },
 ]

De forma que tendremos un servicio expuesto en la URL /api/posts de nuestro servidor. Este servicio hará uso de dos modulos de NodeJs. El primero de ellos es el módulo nativo de node para manejar ficheros “fs” y como queremos extraer cierta información de los ficheros md, usaremos el módulo “front-matter”

El objetivo es leer el directorio contents/blog y devolver un JSON con el listado de posts, por lo que el código quedaría tal que así:

import fs from 'fs'
import fm from 'front-matter'

export default async function (req, res, next) {

  const posts = [];
  const dirs = fs.readdirSync('./contents/blog')

  dirs.forEach(function (file) {
    let data = fs.readFileSync('./contents/blog/' + file)
    let attributes = (fm(data.toString())).attributes
    posts.push(
      {
        title: attributes.title,
        thumb: attributes.thumb || "",
        category: attributes.category || "General",
        slug: "/blog/" + file.replace(".md", "")
      }
    )
  })

  res.json(posts)
}

Ahora en nuestra vista (blog/index.vue) simplemente tendremos que llamar a nuestro servicio. Nuevamente lo hacemos desde nuestro método asyncData

async asyncData ({ params, error, $axios }) {

      try {
        let response = await $axios('/api/posts')
        return {
          posts: response.data
        }
      } catch (e) {
        error({ statusCode: 404, message: 'No hemos podido cargar los artículos' })
      }
    }

Para la carga decidimos usar $axios ya que lo usamos previamente en la web de codetakers, no obstante con Fetch hubiera sido más que suficiente.

Despliegue

A la hora de subir nuestro blog (y en general la web de codetakers), hemos usado un servidor personalizado para renderizar nuestras páginas SSR frente al servidor por defecto que integra Nuxt.

El motivo de esta decisión es por la necesidad de usar los serverMiddlewares que mencionamos anteriormente. Por ello, configuramos un servidor basado en express

const express = require('express')
const consola = require('consola')
const { Nuxt, Builder } = require('nuxt')
const app = express()

app.use(express.urlencoded({ extended: false }))
app.use(express.json());

const config = require('../nuxt.config.js')
config.dev = process.env.NODE_ENV !== 'production'

async function start () {
  const nuxt = new Nuxt(config)
  const { host, port } = nuxt.options.server
  if (config.dev) {
    const builder = new Builder(nuxt)
    await builder.build()
  } else {
    await nuxt.ready()
  }
  app.use(nuxt.render)
  app.listen(port, host)
  consola.ready({
    message: `Server listening on http://${host}:${port}`,
    badge: true
  })
}
start()

A diferencia del servidor por defecto de Nuxt, éste nos permite añadir el middleware de express “body-parser” que usamos para gestionar la entrada y salida de nuestra API.

Una vez ponemos nuestro servidor en producción, solo debemos crear un proxy inverso con Nginx que “conecte” el puerto 80 y 443 (SSL) con el puerto de nuestro servidor express (puerto 3000).

Para mantener levantado el servidor NodeJS hemos optado por PM2 que nos permite levantar los servicios fácilmente.

A la hora de realizar el despliegue nos hemos creado un script bash que, en local, nos genera un artefacto de nuestra aplicación mediante el comando yarn build y subimos mediante rsync a nuestro servidor y ejecutamos pm2 start

Pendiente nos queda automatizar un poco más esta tarea y hacer que todo quede desplegado por cada merge en master, pero esto lo dejamos para la siguiente entrega ;)