Comprueba que tienes Node (una versión ≥ 14), Yarn y un editor de código instalado, yo suelo trabajar con VS Code. De no ser así, instálalos haciendo click en cada enlace.
También estaría bien que tuvieras instalada la extensión Vue Devtools en tu navegador, que nos ayudará a revisar como actúa nuestra aplicación. Y las extensiones Vetur y Vue VSCode Snippets para nuestro IDE.
Para crear el repositorio donde desarrollaremos la aplicación vamos a usar como template el repositorio → https://github.com/Dawntraoz/vue-nuxt-boilerplate.
Use as Template nos crea una copia de ese proyecto en ese momento, para poder seguir desarrollando a partir de ese proyecto sin tener que instalar todo desde cero.
Una vez hemos creado el repositorio, lo descargamos para trabajarlo localmente ejecutando el comando git clone <url_del_repo>
, en el terminal que solamos utilizar.
Una vez clonado, navegamos a la carpeta con cd <nombre_del_repo>
y ejecutamos yarn install
para instalar todos los paquetes.
Nos creamos una cuenta en Storyblok y creamos nuestro Space.
Para poder utilizarlo localmente con nuestro nuevo repositorio, ves a Settings:
En API-Keys copia el valor que aparece en pantalla.
En General > Location (default environment) cambia la URL por *http://localhost:3000/*.
Crea un archivo .env en el root de tu proyecto de Nuxt, añade una variable llamada STORYBLOK_TOKEN e iguálala a la API-Key que copiaste en el paso anterior. Por último, actualiza el paquete en nuxt.config.js con la variable recién creada:
[
'@storyblok/nuxt',
{
accessToken: process.env.STORYBLOK_TOKEN,
cacheProvider: 'memory'
}
],
Ahora que todo está iniciado y conectado, es momento de dar un vistazo al panel de Storyblok, revisar los componentes, y como usarlos en la sección Content.
Antes de empezar con la chicha, démosle un nombre y una descripción acorde a nuestro
title
ydescription
en nuxt.config.js y alname
package.json.
Empecemos esta aventura por el layout, ese lugar donde definiremos componentes globales que compartirán nuestras páginas como el Header y el Footer.
Dado que cada página tiene el mismo menú de navegación, para no añadir un componente de este tipo cada vez que creemos una nueva entrada, utilizamos un recurso global que definiremos en Layout.
Primero crearemos los componentes más pequeños y reutilizables, en este caso átomos y moléculas, que utilizaremos tanto en el Header como en el Footer, y seguramente en otras partes de la web.
Creamos las carpetas atoms, molecules, organisms y templates.
Añadimos el Content Type ya creado, page, en la carpeta templates haciendo drag & drop.
Creamos a-link en la carpeta atoms como componente Nestable.
En su esquema añadimos un campo URL de tipo Link y un campo name de tipo Text.
Creamos m-link-list en la carpeta molecules como componente Nestable.
En su esquema definimos un campo llamado links tipo Blocks, permitiendo solo añadir componentes tipo a-link (el átomo que acabamos de crear).
Una vez tenemos el átomo y la molécula definidos, creamos un componente Content Type llamado layout en la carpeta templates (de tipo Content Type y no Nestable).
En el schema añadimos header_links y footer_links de tipo Blocks, solo permitiendo m-link-list como componente.
En el caso de header_links, lo ideal sería permitir como máximo 1 m-link-list únicamente, pero en el caso de footer_links tendría sentido tener varios por si queremos añadir múltiples columnas.
Por último, vamos a la sección Content del panel para crear nuestro Layout e introducir su contenido. Haz click en el botón + Entry y elige layout como Content Type.
Añade unos cuantos links en ambos campos:
¡Ya es momento de representarlo en código! Vamos a obtener los datos del layout desde la store de nuestro proyecto de Nuxt.
Antes de empezar creamos las carpetas en components equivalentes a las que hemos creado en Storyblok: atoms, molecules, organisms y templates. Y movemos Page.vue a la carpeta templates, actualizando el plugin components.js con la nueva ubicación.
Ahora sí, empecemos a darle vida a nuestros componentes:
En la carpeta organisms creamos AppHeader.vue y AppFooter.vue y los añadimos en el default.vue de la carpeta layouts:
<template>
<div class="flex flex-col min-h-screen bg-indigo-50 pt-6">
<AppHeader />
<Nuxt class="flex-auto" />
<AppFooter />
</div>
</template>
Para que Nuxt los autoimporte necesitamos definir las carpetas en la variable components en nuxt.config.js.
components: [
'~/components',
'~/components/atoms',
'~/components/molecules',
'~/components/organisms',
'~/components/templates',
]
En la carpeta store creamos un archivo index.js, y añadimos la llamada a la API de Storyblok, más concretamente a la página Layout que hemos creado. Para ello añade el código siguiente:
export const state = () => ({
layout: null
})
export const actions = {
async updateLayout({ commit }) {
const { data } = await this.$storyapi.get(`cdn/stories/layout`, {
version: this.app.context.query._storyblok || this.app.context.isDev ? 'draft' : 'published'
});
commit('UPDATE_LAYOUT', data.story)
}
}
export const mutations = {
UPDATE_LAYOUT(state, layoutData) {
state.layout = layoutData
}
}
En la acción updateLayout veremos la llamada y la versión draft o published dependiendo del entorno, local o de producción. Al añadir el condicional comprobando si estamos en local veremos cambios en Storyblok que no estarán publicados, para revisar antes de subir.
Para llamar a la acción creada, vamos a la vista _.vue en pages y añadimos el código siguiente en asyncData, pero antes ponemos async justo antes de asyncData:
if(!context.store.state.layout) { // Si layout es null, hacemos la llamada
await context.store.dispatch('updateLayout')
}
Ahora que ya hemos obtenido los datos del layout vamos a representarlos en el AppHeader y AppFooter que hemos creado.
<!-- AppHeader -->
<template>
<header v-if="headerLinks" class="container mx-auto px-4">
</header>
</template>
<script>
export default {
computed: {
headerLinks () {
return this.$store.state.layout?.content?.header_links[0]
}
}
}
</script>
<!-- AppFooter -->
<template>
<footer v-if="footerLinks" class="bg-indigo-900 text-white py-8 md:py-12">
</footer>
</template>
<script>
export default {
computed: {
footerLinks () {
return this.$store.state.layout?.content?.footer_links
}
}
}
</script>
Lo siguiente que vamos a necesitar es representar los listados de links que hemos creado para ambos componentes. Creamos, en la carpeta molecules, el componente MLinkList.vue.
<template>
<ul
v-editable="blok" class="flex -mr-4">
<li
:key="blok._uid"
v-for="blok in blok.links"
class="flex-auto py-1 pr-4">
<component :blok="blok" :is="blok.component" />
</li>
</ul>
</template>
<script>
export default {
props: {
blok: {
type: Object,
required: true
}
}
}
</script>
El componente dinámico anterior será, en este caso, ALink.vue, el cual vamos a crear en la carpeta atoms, copia el código siguiente:
<template>
<component
v-editable="blok"
:is="externalLink ? 'a' : 'nuxt-link'"
:target="externalLink && '_blank'"
:href="externalLink && urlLink"
:to="!externalLink && urlLink"
:rel="externalLink && 'noopener noreferrer'"
>
{{ blok.name }}
</component>
</template>
<script>
export default {
props: {
blok: {
type: Object,
required: true
}
},
computed: {
externalLink () {
return this.blok.url.linktype === 'url'
},
urlLink () {
const cachedUrl = this.blok.url.cached_url
return '/' + cachedUrl === 'home' ? '' : cachedUrl
}
}
}
</script>
<style scoped>
.nuxt-link-exact-active {
@apply border-b-2 border-indigo-500;
}
</style>
Una vez ambos componentes han sido definidos, podemos añadir el contenido a AppHeader y AppFooter:
<!-- AppHeader -->
<template>
<header v-if="headerLinks" class="container mx-auto px-4">
<nav class="flex items-center justify-between bg-white rounded-md shadow-xl p-4 md:px-6">
<component :blok="headerLinks" :is="headerLinks.component" />
</nav>
</header>
</template>
<!-- AppFooter -->
<template>
<footer v-if="footerLinks" class="bg-indigo-900 text-white py-8 md:py-12">
<div class="container mx-auto px-4 flex flex-wrap">
<component
v-for="column in footerLinks"
:key="column._uid"
:blok="column"
:is="column.component"
class="flex-col w-full md:w-1/2 lg:w-1/3"
/>
</div>
</footer>
</template>
Ten en cuenta que todos los componentes que usemos de manera dinámica será necesario definirlos en el archivo components.js en la carpeta plugins, ya que Nuxt no podrá importarlos dinámicamente al no tener el nombre del componente especificado en el template.
Así debería quedar nuestro archivo de componentes:
import Vue from 'vue'
/* Atoms */
import ALink from '~/components/atoms/ALink.vue'
/* Molecules */
import MLinkList from '~/components/molecules/MLinkList.vue'
/* Templates */
import Page from '~/components/templates/Page.vue'
import Teaser from '~/components/Teaser.vue'
import Grid from '~/components/Grid.vue'
import Feature from '~/components/Feature.vue'
Vue.component('a-link', ALink)
Vue.component('m-link-list', MLinkList)
Vue.component('page', Page)
Vue.component('teaser', Teaser)
Vue.component('grid', Grid)
Vue.component('feature', Feature)
Rediseña y añade HTML semántico a los componentes que ya existían Grid, Feature y Teaser. Cambiales el nombre para ser Moléculas y Organismos añadiendo m- y o- delante, tanto en el código como en el space de Storyblok, y muévelos a su carpeta correspondiente en ambos sitios.
AHeading: Es hora de definir el átomo Heading, porque necesitaremos cabeceras en alguna molécula así que vamos a encapsular su estilo y su funcionamiento en un solo componente:
// AHeading.vue
<template>
<component :is="tag" class="font-bold pb-2">
{{ content }}
</component>
</template>
<script>
export default {
props: {
tag: {
type: String,
required: true,
},
content: {
type: String,
required: true,
},
},
}
</script>
ARichText: Hacemos lo mismo para el contenido de texto de nuestras moléculas, para un campo tipo Rich Text en Storyblok. En este átomo parsearemos su contenido con el método richTextResolver.render() proporcionado por el módulo de Storyblok para Nuxt:
Este artículo explica cómo usar el richTextResolver en Nuxt https://www.storyblok.com/faq/how-to-render-richtext-nuxt
// ARichText.vue
<template>
<div v-if="contentHTML" v-html="contentHTML" class="prose"></div>
</template>
<script>
export default {
props: {
content: {
type: [Object, String],
required: true
}
},
computed: {
contentHTML() {
return this.content ? this.$storyapi.richTextResolver.render(this.content) : ''
}
}
}
</script>
La clase prose
que podéis ver en la etiqueta div
del átomo es parte del plugin Typography de TailwindCSS. Para que los bloques de código, las citas, las diferentes cabeceras, imágenes y espaciado que provengan de RichText tengan el estilo apropiado instalamos el plugin Tailwind Typography y lo añadimos en nuxt.config.js:
# Install
yarn add -D @tailwindcss/typography
// Nuxt.config.js
import tailwindTypography from '@tailwindcss/typography';
export default {
// ...
tailwindcss: {
config: {
plugins: [tailwindTypography],
}
}
}
Para más info consultar el artículo https://nicksaraev.com/how-to-use-tailwind-typography-with-nuxtjs/
Así es cómo quedarían entonces los componentes remaquetados que ya venían en el proyecto usado como template, pero que hemos refactorizado:
MTeaser:
// MTeaser.vue
<template>
<header
v-editable="blok"
class="container mx-auto px-4 py-16"
>
<a-heading tag="h1" :content="blok.headline" class="text-5xl pb-8" />
<a-rich-text :content="blok.description" class="max-w-none" />
</header>
</template>
<script>
export default {
props: {
blok: {
type: Object,
required: true
}
}
}
</script>
MFeature:
// MFeature.vue
<template>
<article
v-editable="blok"
class="border border-indigo-300 bg-white p-6"
>
<a-heading tag="h2" :content="blok.name" class="uppercase" />
</article>
</template>
<script>
export default {
props: {
blok: {
type: Object,
required: true
}
}
}
</script>
OGrid:
// OGrid.vue
<template>
<section
v-editable="blok"
class="bg-indigo-100 py-8 md:py-12">
<div class="container px-4 mx-auto flex flex-wrap">
<div
:key="blok._uid"
v-for="blok in blok.columns"
class="w-full md:w-1//2 lg:w-1/3 p-2">
<component :blok="blok" :is="blok.component" />
</div>
</div>
</section>
</template>
<script>
export default {
props: {
blok: {
type: Object,
required: true
}
}
}
</script>
Y, finalmente, así quedaría el plugin components con todo actualizado y ordenado:
import Vue from 'vue'
/* Atoms */
import ALink from '~/components/atoms/ALink.vue'
/* Molecules */
import MLinkList from '~/components/molecules/MLinkList.vue'
import MFeature from '~/components/molecules/MFeature.vue'
import MTeaser from '~/components/molecules/MTeaser.vue'
/* Organisms */
import OGrid from '~/components/organisms/OGrid.vue'
/* Templates */
import Page from '~/components/templates/Page.vue'
Vue.component('a-link', ALink)
Vue.component('m-link-list', MLinkList)
Vue.component('m-feature', MFeature)
Vue.component('m-teaser', MTeaser)
Vue.component('o-grid', OGrid)
Vue.component('page', Page)
Al haber creado dinámicamente el menú del Header y el Footer, el Crawler de Nuxt no nos detectará las rutas y por lo tanto deberemos ayudarnos de la API de Storyblok y del campo generate
de nuxt.config.js.
// nuxt.config.js
import axios from 'axios';
export default {
generate: {
routes(callback) {
const routesToIgnore = ['home', 'layout']
let cache_version = 0
let routes = ['/']
// Load space and receive latest cache version key to improve performance
axios.get(
`https://api.storyblok.com/v2/cdn/spaces/me?token=${process.env.STORYBLOK_TOKEN}`
).then((space_res) => {
// timestamp of latest publish
cache_version = space_res.data.space.version
// Call for all Links using the Links API: <https://www.storyblok.com/docs/Delivery-Api/Links>
axios.get(`https://api.storyblok.com/v2/cdn/links?token=${process.env.STORYBLOK_TOKEN}&version=published&cv=${cache_version}&per_page=100`).then((res) => {
Object.keys(res.data.links).forEach((key) => {
if (!routesToIgnore.includes(res.data.links[key].slug)) {
routes.push('/' + res.data.links[key].slug)
}
})
callback(null, routes)
})
})
}
}
}
Técnica sacada de la documentación de Storyblok-Nuxt https://www.storyblok.com/faq/how-to-generate-routes-for-nuxt-js-using-storyblok
He escogido Netlify como Hosting para este ejemplo porque me lo da todo hecho y estoy familiarizada.
Una vez tengamos una cuenta creada, nos permitirá crear un nuevo sitio a partir de un repositorio en Github, BitBucket o Gitlab. Es tan sencillo que solo tendremos que facilitarle la siguiente información y lo publicará sin más:
El repositorio que queremos publicar: repo ejemplo.
La rama que hará de trigger y lanzará la subida: master.
El comando que construirá nuestro proyecto, yarn generate
en nuestro caso.
La carpeta que habrá que leer una vez construido: dist.
Las variables de entorno utilizadas en el proyecto: STORYBLOK_TOKEN.
¡Ya está listo para ser publicado! Hacemos click en Deploy Site y lo tenemos 👏
Ahora solo nos queda automatizar la subida cuando publiquemos nuevos cambios del contenido en Storyblok.
En Netlify vamos a Deploy Settings > Build hooks
y añadimos uno llamado Storyblok Published:
La URL generada la copiamos y vamos al panel de Storyblok a Settings > Webhooks, lo pegamos en el campo Story published & unpublished:
¡Guardamos los cambios y vamos a probar ese deploy automático! 🚀
Al publicar contenido nuevo, veremos en nuestro hosting Deploy triggered by hook con el nombre que le hemos dado. Y así nuestros cambios serán publicados sin estar pendientes de pulsar ningún botón.