Cómo mi sitio web basado en API me ayuda a viajar por el mundo

Publicado: 2022-03-10
Resumen rápido ↬ Después de crear varios sitios web de clientes, que van desde pequeños cafés hasta nuevas empresas en crecimiento, Stefan Judis descubrió que el santo editor WYSIWYG no siempre es la bala de plata que todos estamos buscando. Estas interfaces tienen como objetivo facilitar la creación de sitios web, pero siempre hay más casos de uso para su contenido en diferentes plataformas.

(Esta es una publicación patrocinada). Recientemente, decidí reconstruir mi sitio web personal, porque tenía seis años y parecía, cortésmente hablando, un poco "anticuado". El objetivo era incluir información sobre mí, un área de blog, una lista de mis proyectos paralelos recientes y próximos eventos.

Como trabajo con el cliente de vez en cuando, había una cosa con la que no quería lidiar: ¡las bases de datos ! Anteriormente, construí sitios de WordPress para todos los que querían que lo hiciera. La parte de programación solía ser divertida para mí, pero los lanzamientos, el traslado de bases de datos a diferentes entornos y la publicación real siempre eran molestos. Los proveedores de alojamiento baratos solo ofrecen interfaces web deficientes para configurar bases de datos MySQL y un acceso FTP para cargar archivos siempre fue la peor parte. No quería lidiar con esto para mi sitio web personal.

Así que los requisitos que tenía para el rediseño eran:

  • Una pila de tecnología actualizada basada en JavaScript y tecnologías frontend.
  • Una solución de administración de contenido para editar contenido desde cualquier lugar.
  • Un sitio de buen rendimiento con resultados rápidos.

En este artículo quiero mostrarte lo que construí y cómo mi sitio web sorprendentemente resultó ser mi compañero diario.

Definición de un modelo de contenido

Publicar cosas en la web parece ser fácil. Elija un sistema de administración de contenido (CMS) que proporcione un editor WYSIWYG ( Lo que ve es lo que obtiene) para cada página que se necesita y todos los editores pueden administrar el contenido fácilmente. Eso es todo, ¿verdad?

Después de construir varios sitios web de clientes, que van desde pequeños cafés hasta nuevas empresas en crecimiento, descubrí que el santo editor WYSIWYG no siempre es la panacea que todos estamos buscando. Estas interfaces tienen como objetivo facilitar la creación de sitios web, pero aquí viene el punto:

Crear sitios web no es fácil

Para construir y editar el contenido de un sitio web sin romperlo constantemente, debe tener un conocimiento profundo de HTML y al menos comprender un poco de CSS. Eso no es algo que pueda esperar de sus editores.

He visto horribles diseños complejos creados con editores WYSIWYG y no puedo comenzar a nombrar todas las situaciones en las que todo se desmorona porque el sistema es demasiado frágil. Estas situaciones conducen a peleas e incomodidad donde todas las partes se culpan mutuamente por algo que era inevitable. Siempre traté de evitar estas situaciones y crear entornos cómodos y estables para que los editores evitaran correos electrónicos enojados que gritaban: “¡Ayuda! Todo está roto."

El contenido estructurado le ahorra algunos problemas

Aprendí bastante rápido que las personas rara vez rompen las cosas cuando divido todo el contenido necesario del sitio web en varios fragmentos, cada uno relacionado entre sí sin pensar en ninguna representación. En WordPress, esto se puede lograr usando tipos de publicaciones personalizadas. Cada tipo de publicación personalizada puede incluir varias propiedades con su propio campo de texto fácil de entender. Enterré completamente el concepto de pensar en páginas .

Las configuraciones de WordPress con tipos de publicaciones personalizadas facilitan la edición.

Mi trabajo consistía en conectar las piezas de contenido y crear páginas web a partir de estos bloques de contenido. Esto significaba que los editores solo podían hacer pocos cambios visuales, si es que los había, en sus sitios web. Eran responsables del contenido y solo del contenido. Los cambios visuales los tenía que hacer yo: no todo el mundo podía diseñar el sitio y podíamos evitar un entorno frágil. Este concepto se sintió como una gran compensación y generalmente fue bien recibido.

Más tarde descubrí que lo que estaba haciendo era definir un modelo de contenido. Rachel Lovinger define, en su excelente artículo “Content Modelling: A Master Skill”, un modelo de contenido de la siguiente manera:

“Un modelo de contenido documenta todos los diferentes tipos de contenido que tendrá para un proyecto determinado. Contiene definiciones detalladas de los elementos de cada tipo de contenido y sus relaciones entre sí”.

Comenzar con el modelado de contenido funcionó bien para la mayoría de los clientes, excepto para uno.

“¡Stefan, no estoy definiendo el esquema de tu base de datos!”

La idea de este proyecto era crear un sitio web masivo que generara una gran cantidad de tráfico orgánico al proporcionar toneladas de contenido, en todas las variaciones que se muestran en varias páginas y lugares diferentes. Organicé una reunión para discutir nuestra estrategia para abordar este proyecto.

Quería definir todas las páginas y modelos de contenido que deberían incluirse. No importaba qué pequeño widget o qué barra lateral tenía en mente el cliente, quería que estuviera claramente definido. Mi objetivo era crear una estructura de contenido sólida que hiciera posible proporcionar una interfaz fácil de usar para los editores y proporcionar datos reutilizables para mostrarlos en cualquier formato imaginable.

Resultó que la idea de este proyecto no estaba muy clara y no pude obtener respuestas a todas mis preguntas. El líder del proyecto no entendió que deberíamos comenzar con el modelado de contenido adecuado (no con el diseño y desarrollo). Para él, esto era solo un montón de páginas. El contenido duplicado y las grandes áreas de texto para agregar una gran cantidad de texto no parecían ser un problema. En su mente, las preguntas que tenía sobre la estructura eran técnicas y no deberían preocuparse por ellas. Para resumir, no hice el proyecto.

Lo importante es que el modelado de contenido no se trata de bases de datos.

Se trata de hacer que su contenido sea accesible y esté preparado para el futuro. Si no puede definir las necesidades de su contenido al inicio del proyecto, será muy difícil, si no imposible, reutilizarlo más adelante.

El modelado de contenido adecuado es la clave para los sitios web presentes y futuros.

Con contenido: un CMS sin cabeza

Estaba claro que también quería seguir un buen modelo de contenido para mi sitio. Sin embargo, había una cosa más. No quería lidiar con la capa de almacenamiento para construir mi nuevo sitio web, así que decidí usar Contentful, un CMS sin encabezado, en el que (¡descargo de responsabilidad completo!) Estoy trabajando actualmente. “Headless” significa que este servicio ofrece una interfaz web para administrar el contenido en la nube y proporciona una API que me devolverá mis datos en formato JSON. Elegir este CMS me ayudó a ser productivo de inmediato, ya que tenía una API disponible en minutos y no tuve que lidiar con ninguna configuración de infraestructura. Contentful también proporciona un plan gratuito que es perfecto para proyectos pequeños, como mi sitio web personal.

Una consulta de ejemplo para obtener todas las publicaciones del blog se ve así:

 <a href="https://cdn.contentful.com/spaces/space_id/entries?access_token=access_token&content_type=post">https://cdn.contentful.com/spaces/space_id/entries?access_token=access_token&content_type=post</a>

Y la respuesta, en una versión abreviada, se ve así:

 { "sys": { "type": "Array" }, "total": 7, "skip": 0, "limit": 100, "items": [ { "sys": { "space": {...}, "id": "455OEfg1KUskygWUiKwmkc", "type": "Entry", "createdAt": "2016-07-29T11:53:52.596Z", "updatedAt": "2016-11-09T21:07:19.118Z", "revision": 12, "contentType": {...}, "locale": "en-US" }, "fields": { "title": "How to React to Changing Environments Using matchMedia", "excerpt": "...", "slug": "how-to-react-to-changing-environments-using-match-media", "author": [...], "body": "...", "date": "2014-12-26T00:00+02:00", "comments": true, "externalUrl": "https://4waisenkinder.de/blog/2014/12/26/handle-environment-changes-via-window-dot-matchmedia/" }, {...}, {...}, {...}, {...}, {...}, {...} ] } }

La gran parte de Contentful es que es genial en el modelado de contenido, lo cual necesitaba. Usando la interfaz web provista, puedo definir todas las piezas de contenido necesarias rápidamente. La definición de un modelo de contenido particular en Contentful se denomina tipo de contenido. Una gran cosa a destacar aquí es la capacidad de modelar las relaciones entre los elementos de contenido. Por ejemplo, puedo conectar fácilmente a un autor con una publicación de blog. Esto puede dar como resultado árboles de datos estructurados, que son perfectos para reutilizar para varios casos de uso.

Configuración de un tipo de contenido mediante el editor de modelo de contenido (vista previa grande)

Entonces, configuré mi modelo de contenido sin pensar en ninguna página que pueda querer construir en el futuro.

Modelo de contenido para el sitio web.

El siguiente paso fue averiguar qué quería hacer con estos datos. Le pregunté a un diseñador que conocía y me presentó una página de índice del sitio web con la siguiente estructura.

Maqueta para la página de índice del sitio web.

Representación de páginas HTML con Node.js

Ahora venía la parte complicada. Hasta ahora, no tenía que lidiar con el almacenamiento y las bases de datos, lo que fue un gran logro para mí. Entonces, ¿cómo puedo construir mi sitio web cuando solo tengo una API disponible?

Mi primer enfoque fue el enfoque de hágalo usted mismo. Comencé a escribir un script simple de Node.js que recuperaría los datos y generaría algo de HTML a partir de ellos.

La representación de todos los archivos HTML por adelantado cumplió con uno de mis requisitos principales. El HTML estático se puede servir muy rápido.

Entonces, echemos un vistazo al script que usé.

 'use strict'; const contentful = require('contentful'); const template = require('lodash.template'); const fs = require('fs'); // create contentful client with particular credentials const client = contentful.createClient({ space: 'your_space_id', accessToken: 'your_token' }); // cache templates to not read // them over and over again const TEMPLATES = { index : template(fs.readFileSync(`${__dirname}/templates/index.html`)) }; // fetch all the data Promise.all([ // get posts client.getEntries({content_type: 'content_type_post_id'}), // get events client.getEntries({content_type: 'content_type_event_id'}), // get projects client.getEntries({content_type: 'content_type_project_id'}), // get talk client.getEntries({content_type: 'content_type_talk_id'}), // get specific person client.getEntries({'sys.id': 'person_id'}) ]) .then(([posts, events, projects, talks, persons]) => { const renderedHTML = TEMPLATES.index({ posts, events, projects, talks, person : persons.items[0] }) fs.writeFileSync(`${__dirname}/build/index.html`, renderedHTML); console.log('Rendered HTML'); }) .catch(console.error);
 <!doctype html> <html lang="en"> <head> <!-- ... --> </head> <body> <!-- ... --> <h2>Posts</h2> <ul> <% posts.items.forEach( function( talk ) { %> <li><%- talk.fields.title %> <% }) %> </ul> <!-- ... --> </body> </html>

Esto funcionó bien. Podría construir mi sitio web deseado de una manera completamente flexible, tomando todas las decisiones sobre la estructura y funcionalidad del archivo. La representación de diferentes tipos de página con conjuntos de datos completamente diferentes no supuso ningún problema. Cualquiera que haya luchado contra las reglas y la estructura de un CMS existente que se envía con representación HTML sabe que la libertad total puede ser algo excelente. Especialmente, cuando el modelo de datos se vuelve más complejo con el tiempo e incluye muchas relaciones, la flexibilidad vale la pena.

En este script de Node.js, se crea un cliente SDK con contenido y todos los datos se recuperan mediante el método de cliente getEntries . Todos los métodos proporcionados por el cliente están basados ​​en promesas, lo que facilita evitar devoluciones de llamada profundamente anidadas. Para las plantillas, decidí usar el motor de plantillas de lodash. Finalmente, para la lectura y escritura de archivos, Node.js ofrece el módulo fs nativo, que luego se usa para leer las plantillas y escribir el HTML renderizado.

Sin embargo, había una desventaja en este enfoque; era muy básico. Incluso cuando este método era completamente flexible, se sintió como reinventar la rueda. Lo que estaba construyendo era básicamente un generador de sitios estáticos, y ya hay muchos de ellos. Era hora de empezar todo de nuevo.

Ir a por un generador de sitio estático real

Los generadores de sitios estáticos famosos, por ejemplo, Jekyll o Middleman, generalmente se ocupan de los archivos Markdown que se procesarán en HTML. Los editores trabajan con estos y el sitio web se construye usando un comando CLI. Sin embargo, este enfoque estaba fallando en uno de mis requisitos iniciales. Quería poder editar el sitio dondequiera que estuviera, sin depender de los archivos que se encuentran en mi computadora privada.

Mi primera idea fue renderizar estos archivos Markdown usando la API. Aunque esto hubiera funcionado, no se sentía bien. Renderizar archivos de Markdown para transformarlos a HTML más tarde aún eran dos pasos que no ofrecían un gran beneficio en comparación con mi solución inicial.

Afortunadamente, existen integraciones Contentful, por ejemplo, Metalsmith y Middleman. Me decidí por Metalsmith para este proyecto, ya que está escrito en Node.js y no quería incorporar una dependencia de Ruby.

Metalsmith transforma archivos de una carpeta de origen y los representa en una carpeta de destino. Estos archivos no necesariamente tienen que ser archivos Markdown. También puede usarlo para transpilar Sass u optimizar sus imágenes. No hay límites, y es realmente flexible.

Usando la integración Contentful, pude definir algunos archivos de origen que se tomaron como archivos de configuración y luego pude obtener todo lo necesario de la API.

 --- title: Blog contentful: content_type: content_type_id entry_filename_pattern: ${ fields.slug } entry_template: article.html order: '-fields.date' filter: include: 5 layout: blog.html description: >- Recent articles by Stefan Judis. ---

Esta configuración de ejemplo muestra el área de publicación del blog con un archivo principal blog.html , incluida la respuesta de la solicitud de la API, pero también muestra varias páginas secundarias con la plantilla article.html . Los nombres de archivo para las páginas secundarias se definen a través entry_filename_pattern .

Como ves, con algo como esto, puedo construir mis páginas fácilmente. Esta configuración funcionó perfectamente para garantizar que todas las páginas dependieran de la API.

Conecte el servicio con su proyecto

La única parte que faltaba era conectar el sitio con el servicio CMS y hacer que se volviera a mostrar cuando se editara cualquier contenido. La solución para este problema: webhooks, con los que quizás ya esté familiarizado si está utilizando servicios como GitHub.

Los webhooks son solicitudes realizadas por software como servicio a un punto final previamente definido que le notifican que algo ha sucedido. GitHub, por ejemplo, puede enviarle un ping cuando alguien abrió una solicitud de extracción en uno de sus repositorios. Con respecto a la gestión de contenido, podemos aplicar el mismo principio aquí. Cada vez que algo suceda con el contenido, haga ping a un punto final y haga que un entorno en particular reaccione. En nuestro caso, esto significaría volver a renderizar el HTML usando metalsmith.

Para aceptar webhooks, también opté por una solución de JavaScript. El proveedor de hospedaje de mi elección (Uberspace) hace posible instalar Node.js y usar JavaScript en el lado del servidor.

 const http = require('http'); const exec = require('child_process').exec; const server = http.createServer((req, res) => { res.setHeader('Content-Type', 'text/plain'); // check for secret header // to not open up this endpoint for everybody if (req.headers.secret === 'YOUR_SECRET') { res.end('ok'); // wait for the CDN to // invalidate the data setTimeout(() => { // execute command exec('npm start', { cwd: __dirname }, (error) => { if (error) { return console.log(error); } console.log('Rebuilt success'); }); }, 1000 * 120 ); } else { res.end('Not allowed'); } }); console.log('Started server at 8000'); server.listen(8000);

Esta secuencia de comandos inicia un servidor HTTP simple en el puerto 8000. Verifica las solicitudes entrantes en busca de un encabezado adecuado para asegurarse de que sea el webhook de Contentful. Si la solicitud se confirma como webhook, se ejecuta el comando predefinido npm start para volver a representar todas las páginas HTML. Quizás se pregunte por qué existe un tiempo de espera. Esto es necesario para pausar las acciones por un momento hasta que los datos en la nube se invaliden porque los datos almacenados se sirven desde un CDN.

Dependiendo de su entorno, es posible que Internet no pueda acceder a este servidor HTTP. Mi sitio se sirve con un servidor apache, por lo que necesitaba agregar una regla de reescritura interna para que el servidor del nodo en ejecución fuera accesible desde Internet.

 # add node endpoint to enable webhooks RewriteRule ^rerender/(.*) https://localhost:8000/$1 [P]

API-First y datos estructurados: mejores amigos para siempre

En este punto, pude administrar todos mis datos en la nube y mi sitio web reaccionaría en consecuencia después de los cambios.

Repetición por todas partes

Estar de viaje es una parte importante de mi vida, por lo que era necesario tener información, como la ubicación de un lugar determinado o qué hotel reservé, al alcance de la mano, generalmente almacenada en una hoja de cálculo de Google. Ahora, la información estaba distribuida en una hoja de cálculo, varios correos electrónicos, mi calendario y mi sitio web.

Debo admitir que creé mucha duplicación de datos en mi flujo diario.

El momento de los datos estructurados

Soñé con una única fuente de verdad (preferiblemente en mi teléfono) para ver rápidamente qué eventos se avecinaban, pero también para obtener información adicional sobre hoteles y lugares. Los eventos enumerados en mi sitio web no tenían toda la información en este momento, pero es realmente fácil agregar nuevos campos a un tipo de contenido en Contentful. Entonces, agregué los campos necesarios al tipo de contenido "Evento".

Poner esta información en el CMS de mi sitio web nunca fue mi intención, ya que no debería mostrarse en línea, pero tener acceso a ella a través de una API me hizo darme cuenta de que ahora podía hacer cosas completamente diferentes con estos datos.

Adición de más información en los campos de eventos (vista previa grande)

Creación de una aplicación nativa con JavaScript

La creación de aplicaciones para dispositivos móviles ha sido un tema durante años y existen varios enfoques al respecto. Las aplicaciones web progresivas (PWA) son un tema especialmente candente en estos días. Con Service Workers y un Manifiesto de aplicación web, es posible crear experiencias completas similares a las de una aplicación, desde un ícono en la pantalla de inicio hasta un comportamiento fuera de línea administrado mediante tecnologías web.

Hay un inconveniente a mencionar. Las aplicaciones web progresivas están en auge, pero aún no han llegado del todo. Los Service Workers, por ejemplo, no son compatibles con Safari en la actualidad y hasta ahora solo están "bajo consideración" por parte de Apple. Esto fue un factor decisivo para mí, ya que también quería tener una aplicación sin conexión en iPhones.

Así que busqué alternativas. Un amigo mío estaba realmente interesado en NativeScript y no dejaba de hablarme sobre esta tecnología relativamente nueva. NativeScript es un marco de código abierto para crear aplicaciones móviles verdaderamente nativas con JavaScript, así que decidí probarlo.

Conociendo NativeScript

La configuración de NativeScript lleva un tiempo porque tiene que instalar muchas cosas para desarrollar para entornos móviles nativos. Se le guiará a través del proceso de instalación cuando instale la herramienta de línea de comandos de NativeScript por primera vez con npm install nativescript -g .

Luego, puede usar comandos de scaffolding para configurar nuevos proyectos: tns create MyNewApp

Sin embargo, esto no es lo que hice. Estaba escaneando la documentación y encontré una aplicación de administración de comestibles de muestra construida en NativeScript. Así que tomé esta aplicación, busqué en el código y la modifiqué paso a paso, ajustándola a mis necesidades.

No quiero profundizar demasiado en el proceso, pero construir una lista de búsqueda con toda la información que quería, no tomó mucho tiempo.

NativeScript funciona muy bien junto con Angular 2, que no quería probar esta vez porque descubrir que NativeScript en sí era lo suficientemente grande. En NativeScript tienes que escribir "Vistas". Cada vista consta de un archivo XML que define el diseño base y JavaScript y CSS opcionales. Todos estos se definen en una carpeta por vista.

La representación de una lista simple se puede lograr con una plantilla XML como esta:

 <!-- call JavaScript function when ready --> <Page loaded="loaded"> <ActionBar title="All Travels" /> <!-- make it scrollable when going too big --> <ScrollView> <!-- iterate over the entries in context --> <ListView items="{{ entries }}"> <ListView.itemTemplate> <Label text="{{ fields.name }}" textWrap="true" class="headline"/> </ListView.itemTemplate> </ListView> </ScrollView> </Page>

Lo primero que sucede aquí es definir un elemento de página. Dentro de esta página, definí una ActionBar para darle el aspecto clásico de Android, así como un título adecuado. Construir cosas para entornos nativos puede ser un poco complicado a veces. Por ejemplo, para lograr un comportamiento de desplazamiento funcional, debe usar un 'ScrollView'. Lo último es entonces, simplemente iterar sobre mis eventos usando un ListView . En general, ¡se sintió bastante sencillo!

Pero, ¿de dónde provienen estas entradas que se usan en la vista? Resulta que hay un objeto de contexto compartido que se puede usar para eso. Al leer el XML para la vista, es posible que ya haya notado que la página tiene un conjunto de atributos loaded . Al configurar este atributo, le digo a la vista que llame a una función de JavaScript en particular cuando se carga la página.

Esta función de JavaScript se define en el archivo JS dependiente. Se puede hacer accesible simplemente exports.something usando exportaciones.algo. Para agregar el enlace de datos, todo lo que tenemos que hacer es establecer un nuevo Observable en la propiedad de página bindingContext . Los observables en NativeScript emiten eventos propertyChange que son necesarios para reaccionar a los cambios de datos dentro de las vistas, pero no tiene que preocuparse por eso, ya que funciona de forma inmediata.

 const context = new Observable({ entries: null}); const fetchModule = require('fetch'); // export loaded to be called from // List.xml when everything is loaded exports.loaded = (args) => { const page = args.object; page.bindingContext = context; fetchModule.fetch( `https://cdn.contentful.com/spaces/${config.space}/entries?access_token=${config.cda.token}&content_type=event&order=fields.start`, { method: "GET", headers: { 'Content-Type': 'application/json' } } ) .then(response => response.json()) .then(response => context.set('entries', response.items)); }

Lo último es buscar los datos y establecerlos en el contexto. Esto se puede hacer usando el módulo de obtención de fetch . Aquí puedes ver el resultado.

Vista previa grande

Entonces, como puede ver, construir una lista simple usando NativeScript no es realmente difícil. Más tarde amplié la aplicación con otra vista, así como funcionalidades adicionales para abrir direcciones dadas en Google Maps y vistas web para ver los sitios web de eventos.

Una cosa a señalar aquí es que NativeScript todavía es bastante nuevo, lo que significa que los complementos que se encuentran en npm generalmente no tienen muchas descargas o estrellas en GitHub. Esto me irritó al principio, pero utilicé varios componentes nativos (nativescript-floatingactionbutton, nativescript-advanced-webview y nativescript-pulltorfresh) que me ayudaron a lograr una experiencia nativa y todo funcionó perfectamente bien.

Puedes ver el resultado mejorado aquí:

Vista previa grande

Cuanta más funcionalidad puse en esta aplicación, más me gustó y más la usé. La mejor parte es que pude deshacerme de la duplicación de datos, administrar los datos en un solo lugar y ser lo suficientemente flexible como para mostrarlos en varios casos de uso.

Las páginas son ayer: ¡larga vida al contenido estructurado!

La creación de esta aplicación me mostró una vez más que el principio de tener datos en formato de página es cosa del pasado. No sabemos a dónde irán nuestros datos; tenemos que estar preparados para una cantidad ilimitada de casos de uso.

Mirando hacia atrás, lo que logré es:

  • Contar con un sistema de gestión de contenidos en la nube
  • No tener que lidiar con el mantenimiento de la base de datos.
  • Una pila completa de tecnología JavaScript
  • Tener un sitio web estático eficiente
  • Tener una aplicación de Android para acceder a mi contenido en todo momento y lugar

Y la parte más importante:

Tener mi contenido estructurado y accesible me ayudó a mejorar mi vida diaria.

Este caso de uso puede parecerle trivial en este momento, pero cuando piensa en los productos que crea todos los días, siempre hay más casos de uso para su contenido en diferentes plataformas. Hoy en día, aceptamos que los dispositivos móviles finalmente están superando los entornos de escritorio de la vieja escuela, pero plataformas como automóviles, relojes e incluso refrigeradores ya están esperando su atención. Ni siquiera puedo pensar en los casos de uso que vendrán.

Por lo tanto, tratemos de estar preparados y coloquemos contenido estructurado en el medio porque, al final, no se trata de esquemas de bases de datos, se trata de construir para el futuro.

Lectura adicional en SmashingMag:

  • Web Scraping con Node.js
  • Navegando con Sails.js: un marco de estilo MVC para Node.js
  • 40 íconos de viajes para mejorar tus diseños
  • Una introducción detallada a Webpack