Headless

Gestire contenuti statici con Prismic.io e Nuxt.js

Lettura 13 minuti

Nella ricerca di un servizio per la gestione di contenuti statici per un progetto e-commerce headless che stiamo realizzando, la scelta è ricaduta su Prismic.io dopo aver fatto un paio di valutazioni tra questo strumento e Contentful.

Prismic è piuttosto leggero e veloce da configurare e, nel nostro caso, lo utilizziamo per sostituire la parte CMS di Magento, ma è ottimale anche per un sito personale o corporate oppure un blog, anche perché mette a disposizione un appetibile piano free per questi casi.

Metteremo in piedi in questo post una piccola demo di un sito corporate con Nuxt.js e Prismic.io per mostrare le principali caratteristiche di questo strumento.

Partiamo dalla configurazione di Nuxt. Nuxt.js è ormai sulla bocca di tutti nella community di Vue.js e non solo perché ha saputo affermarsi negli ultimi anni come framework Vue solido e funzionale, ma anche per la sua modularità e versatilità, tanto che molte aziende medio-grandi lo stanno ora scegliendo come soluzione headless per il frontend.

Configurazione Nuxt

Per la nostra demo, realizzeremo un sito multi lingua con una pagina “speciale” per l’homepage e una serie di pagine ripetibili per la sezione blog.
Partiamo pertanto dal comodissimo wizard d’installazione che Nuxt ci mette a disposizione:

npx create-nuxt-app corporate-site

E rispondiamo alle domande che ci pone

create-nuxt-app v2.14.0
✨  Generating Nuxt.js project in corporate-site
? Project name corporate-site
? Project description My terrific Nuxt.js project
? Author name Lorena Ramonda
? Choose the package manager Npm
? Choose UI framework None
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules Progressive Web App (PWA) Support, DotEnv
? Choose linting tools ESLint, Prettier, Lint staged files
? Choose test framework None
? Choose rendering mode Universal (SSR)
? Choose development tools jsconfig.json (Recommended for VS Code), Semantic Pull Requests

Siccome, per questo progetto, non andremo a fare altre chiamate API oltre quelle di Prismic, saltiamo l’installazione di Axios poiché, come vedremo, il modulo nuxt-prismic ci mette a disposizione già tutto l’occorente, ma in altri casi, come l’e-commerce menzionato all’inizio del post, è invece molto probabile che si vada ad utilizzare anche Axios o moduli alternativi.

Aggiungiamo giusto una pennellata di stile (che andremo a copiare pedrestremente da questo sito):

npm i -D node-sass sass-loader

e nel nostro nuxt.config.js aggiungiamo:

export default {
  ...
  css: ['@/assets/scss/screen.scss'],
  ...
}

In dettaglio: LINK AL COMMIT

Installiamo anche il modulo i18n per la gestione multi lingua del sito. Per questo esiste il modulo nuxt-i18n:

Installazione:

npm i nuxt-i18n

Configurazione:

nuxt.config.js

/*
  ** Nuxt.js modules
  */
modules: [
  ...
  // Doc: https://nuxt-community.github.io/nuxt-i18n/
  'nuxt-i18n'
],
/**
 * I18n module configuration
 * See https://nuxt-community.github.io/nuxt-i18n/options-reference.html
 */
i18n: {
  locales: [
    {
      name: 'Italiano',
      code: 'it',
      iso: 'it-IT',
      file: 'it.js'
    },
    {
      name: 'English',
      code: 'en',
      iso: 'en-GB',
      file: 'en.js'
    }
  ],
  lazy: true,
  langDir: 'langs/',
  defaultLocale: 'it'
},

Come si evince dalla configurazione andremo a creare un file per lingua all’interno di langs con tutte le traduzioni statiche del sito, per esempio quelle del footer:

langs/it.js

export default {
  corporate: {
    vat: 'p. iva',
    share_capital: 'cap. soc.'
  }
}

langs/en.js

export default {
  corporate: {
    vat: 'vat id',
    share_capital: 'share capital'
  }
}

components/Footer.vue (precedentemente creato)

<template>
  <footer id="footer" role="contentinfo">
    <div class="container">
      <v-socials />
      Bitbull Srl - {{ $t('corporate.vat') }} 03526400043 - {{ $t('corporate.share_capital') }}
      <v-lang-switcher />
    </div>
  </footer>
</template>

Predisponiamo anche uno switcher per fare il cambio lingua:

components/Switcher.vue

<template>
  <ul class="lang">
    <li v-for="locale in $i18n.locales" :key="locale.code" class="lang__item">
      <span v-if="$i18n.locale === locale.code">{{ locale.code }}</span>
      <n-link v-else :to="switchLocalePath(locale.code)" :title="locale.name" exact>
        {{ locale.code }}
      </n-link>
    </li>
  </ul>
</template>

Se tutto funziona, siamo pronti a partire!

PS: Alcuni passaggi nella creazione dei componenti sono stati saltati ma potete trovare tutto qui: LINK AL COMMIT.

Mettiamo un attimo da parte Nuxt e passiamo ora all’argomento padre di questo post.

Integrazione Prismic

Di che cosa avremo bisogno per questo sito?

Innanzitutto, di tre pagine: la homepage (già presente), una pagina blog che elencherà i post, e una pagina dinamica per il singolo post.

Andiamo quindi a creare le altre due pagine mancanti in pages per l’elenco dei post blog/index.vue e per il dettaglio del post blog/_post.vue

In dettaglio: LINK AL COMMIT.

Colleghiamoci ora a Prismic.io e creiamo un account. Una volta loggati ci verrà chiesto di creare un repository. Questo è quello che ospiterà i nostri dati e che ci permette di ottenere il nostro endpoint per le chiamate API. Il nome del repository deve essere univoco in modo assoluto (non è circoscritto al proprio account). Ci verrà anche chiesto di scegliere un piano, partiamo dal free.

Una volta creato lo selezioniamo e ci troveremo nella nostra dashboard del progetto.
Per prima cosa ci verrà chiesto di creare un custom type. Il custom type serve ad identificare i tipi di pagina/contenuto che il nostro sito tratterà. Possono essere di due tipi: ripetibili (repeteable) o unici (single).

Prismic Single Type

Partiamo dal single type e dall’homepage. Come sappiamo l’homepage è una pagina speciale che non viene mai ripetuta su un sito poiché di solito è caratterizzata da una struttura e da uno stile ad hoc. Per cui è la candidata perfetta per il primo custom type di tipo single di Prismic.

Come possiamo vedere, la nostra homepage è composta da un blocco iniziale con un titolo e una descrizione più un blocco sottostante che contiene due blocchi simili quindi potenzialmente ripetibili.

Prismic mette a disposizione tutta una serie di tipi di campi, tra cui titolo, rich text, link etc..
Per questo primo blocco di testo potremmo ipotizzare di utilizzare sia un campo titolo + un campo di rich text oppure anche soltanto un campo di rich text dando la possibilità di utilizzare più tag (che andremo comunque a definire noi). Opterò per quest’ultima opzione per permettere più duttilità nella formulazione del testo e nell’ordine degli elementi, ma nulla vieta di vincolare il tutto ad un ordine prestabilito e tag ridotti.

Salviamo e andiamo a creare subito la nostra homepage aggiungendo i dati che possiamo comodamente copiaincollare.

Compiliamo pertanto il primo campo, salviamo e pubblichiamo (il salvataggio da solo non renderà pubblici i dati nelle API, ma creerà semplicemente una bozza su Prismic). La barra in alto da gialla diventerà verde e ora siamo online!

Come facciamo per recuperare i dati?

Intanto, assicuriamoci che tutto sia andato a buon fine. Se copiamo il proprio personale endpoint e a questo aggiungiamo /api/v2

https://<YOUR-REPO-NAME>.prismic.io/api/v2

ci ritroveremo in una dashboard che permette di interrogare il nostro endpoint.
Da qui possiamo fare un semplice “Search documents” e ritrovarci con tutta la lista dei documenti già creati, oppure interrogarli puntuali tramite la query utilizzando i predicati.

Cliccando in alto il tasto “json” e poi “Search documents” otterremo così un json con i dati, verifichiamo che quanto abbiamo appena inserito sia presente.

C’è ;) quindi possiamo riprendere il nostro progetto in Nuxt ed integrare questi dati all’interno del nostro sito.

Configurazione Prismic su Nuxt

I creatori di Nuxt hanno già pensato a noi e creato un modulo Nuxt che wrappa quello originale e ci facilita le cose.

npm i @nuxtjs/prismic

nuxt.config.js

/*
  ** Nuxt.js modules
  */
modules: [
  ...
  // Doc: https://prismic-nuxt.js.org/docs/getting-started
  '@nuxtjs/prismic'
],

/**
  * Prismic module configuration
  */
prismic: {
  endpoint: 'https://YOURENDPOINT.prismic.io/api/v2',
  linkResolver: '~/prismic/link-resolver.js'
},

Come riporta anche la guida ufficiale del modulo occorre creare anche un file link-resolver.js con questo contenuto:

prismic/link-resolver.js

export default function (doc) {
  return '/'
}

Per maggiori informazioni sul link resolver fare riferimento alla guida Prismic.

Il modulo Nuxt si porta dietro anche i componenti messi a disposizione da Prismic per il parsing di alcuni dei suoi campi che certamente andremo ad usare.

Apriamo ora la nostra home page e facciamo la prima chiamata!

pages/index.vue

async asyncData({ $prismic, error, app }) {
  const doc = await $prismic.api.getSingle('homepage')

  if (doc) {
    return {
      page: doc.data || doc
    }
  } else {
    error({ statusCode: 404, message: 'Page not found' })
  }
},

Per interrogare il nostro repository, facciamo sempre riferimento alla guida di Prismic. Ci sono svariati tipi di query possibili (Query Single Type, by ID & UID, All Documents, by Type, by Tag, by Date, by a Field). Nel caso dell’homepage ci serve interrogare un single type.
Ovviamente, dato che stiamo usando il modulo di Nuxt e non le API dirette di Prismic dobbiamo sì far riferimento alla guida di Prismic ma altresì a quella di NuxtPrismic per sapere cosa ci mette a disposizione. Come si legge nella doc, tutti i metodi che ci servono sono all’interno di $prismic ed ecco che quindi per recuperare i dati dell’homepage possiamo scrivere:

await $prismic.api.getSingle('homepage')

che è un helper che di fatto esegue una query più lunga (che si potrebbe tranquillamente usare al posto):

await $prismic.api.query(Prismic.Predicates.at('document.type', 'homepage'))

Facciamo poi un controllo sul risultato di questa chiamata scatenando una pagina di errore nel caso in cui non si ricevesse nessun dato.

In caso positivo, invece, ci troveremo all’interno della proprietà page in data i dati della nostra pagina che possiamo andare a renderizzare tramite uno dei componenti messi a disposizione da Prismic e menzionati prima, <prismic-rich-text>:

pages/index.vue

<template>
  <div class="container">
    <section class="home-section mission">
      <div class="mission__text">
        <prismic-rich-text v-if="page.mission" :field="page.mission" />
      </div>
      ...
    </section>
  </div>
</template>

Questo renderizzerà correttamente il tag <h2> e <p> che abbiamo utilizzato nel documento senza ulteriore parsing da parte nostra.

Ci ricordiamo che abbiamo deciso di fare un sito multi lingua? Ebbene Prismic gestisce anche la possibilità di tradurre i contenuti in qualsiasi lingua previa pochissima configurazione. Vediamo come.

Ritorniamo in Prismic e clicchiamo in basso a sinistra su “Settings” da qui sul menu laterale “Translations & locales”.
In questa schermata selezioniamo la lingua desiderata e aggiungiamola.

Fatto!

Adesso tornando nella tab dei contenuti vedremo una tendina in più, quella della lingua, e selezionado la lingua appena aggiunta ecco che la nostra homepage vince un badge “to translate” per questa lingua. A questo punto cliccando sull’homepage e aggiungendo i contenuti in lingua avremo la nostra pagina tradotta.

Manca però un controllo nella nostra chiamata a frontend. Torniamo quindi su Nuxt e implementiamolo.

Secondo la guida di Prismic occorre passare alla query un oggetto con il codice iso della lingua in lettere minuscole, ed esempio: { lang : 'fr-fr' }.

Li andremo pertanto a recuperare dalla nostra configurazione Nuxt che ricordiamo essere:

i18n: {
  locales: [
    {
      name: 'Italiano',
      code: 'it',
      iso: 'it-IT',
      file: 'it.js'
    },
    {
      name: 'English',
      code: 'en',
      iso: 'en-GB',
      file: 'en.js'
    }
  ],
}

Per questo, serve creare un filtro js per recuperare l’oggetto della lingua corrente:

app.i18n.locales.filter((lang) => lang.code === app.i18n.locale)[0]

e poi passare l’oggetto con la lingua del solo codice ISO in lowercase:

{
  lang: currentLocale.iso.toLowerCase()
}

pages/index.vue

async asyncData({ $prismic, error, app }) {
  const currentLocale = app.i18n.locales.filter((lang) => lang.code === app.i18n.locale)[0]
  const doc = await $prismic.api.getSingle('homepage', {
    lang: currentLocale.iso.toLowerCase()
  })

  ...
},

LINK AL COMMIT.

Adesso gestiamo i due blocchi partner :)

Per questo ci servirà un gruppo di campi ripetibili.

Per il blocco partner daremo meno libertà per cui andremo ad inserire un campo di tipo “key text” per il titolo, un “rich text” con soltanto la predisposizione di paragrafi, bold e italic, un link e un’immagine.
Salvata la nuova configurazione andiamo a popolare i campi nella parte content (ricordandoci di pubblicare) prima di tornare su Nuxt e completare la pagina (come vedrete essendo un campo ripetibile è possibile crearne più di uno cliccando sulla +).

A questo punto su Nuxt, avendo già fatto tutta la parte precedente, i nuovi dati corrispondenti al partner li abbiamo già all’interno della nostra proprietà page quindi non ci resta altro da fare che andarli a leggere e stamparli.

<section class="home-section partnership">
  <template v-for="partner in page.partner">
    <div :key="partner.name" class="partnership__img">
      <prismic-image :field="partner.image" />
    </div>
    <div :key="partner.name" class="partnership__text">
      <h3 class="partnership__title">{{ partner.name }}</h3>
      <prismic-rich-text v-if="partner.content" :field="partner.content" />
    </div>
  </template>
</section>

Prismic Repeatable Type

Passiamo ora alla gestione dei post, questi in quanto pagine dall’aspetto simile e contenuto diverso, saranno invece dei custom type di tipo ripetibile su Prismic. Andiamo per tanto a creare una struttura per i nostri post con i campi più comuni (uid, titolo, immagine, contenuto, autore).

Lo UID citato in questo caso è vitale per poter poi gestire il dettaglio del post e corrisponderà allo slug dello URL del post.

Per quanto riguarda invece l’autore è possibile crearlo come campo di tipo “Content relationship” e, previo aver creato un custom type ripetibile per l’autore, definirlo come constraint. Questo permetterà in fase di popolazione contenuto di collegare il post al dettaglio autore (che andrà poi gestito a frontend).

Andiamo ora, anche per questo custom type, a creare un contenuto di esempio:

Come dicevo, il campo autore è di tipo content relationship ed è così che si presenta la maschera di selezione una volta che sono stati aggiunti tutti gli autori (in questo caso ce ne sono due).

Creando questo collegamento, sarà possibile, in un’ipotetica pagina autore, effettuare una ricerca di tutti i post ad esso collegato. Ma attenzione! I campi di content relationship si possono interrogare solo tramite document id e non tramite un più user-friendly slug o comunque un campo che possiamo gestire come dato e che possiamo conoscere a priori, ma è un ID generato da Prismic. Quindi possono esserci casi in cui questo tipo di campo risulti non essere la scelta migliore. Ma nel caso nostro, di un blog, va benissimo.

Andiamo ora a recuperare i dati nella nostra pagina di blog.

pages/blog/index.vue

async asyncData({ $prismic, error, app }) {
  const currentLocale = app.i18n.locales.filter((lang) => lang.code === app.i18n.locale)[0]
  const doc = await $prismic.api.query($prismic.predicates.at('document.type', 'post'), {
    orderings: '[document.first_publication_date desc]',
    lang: currentLocale.iso.toLowerCase()
  })

  if (doc) {
    return {
      posts: doc.results || doc
    }
  } else {
    error({ statusCode: 404, message: 'Page not found' })
  }
},

In questo caso, ci serve interrogare tutti i custom type (repeatable lo ricordiamo) di tipo post sempre localizzati e con l’aggiunta di un ordinamento (prima data di pubblicazione decrescente):

pages/blog/index.vue (dettaglio)

$prismic.api.query($prismic.predicates.at('document.type', 'post'), {
  orderings: '[document.first_publication_date desc]',
  lang: currentLocale.iso.toLowerCase()
})

Fatto questo possiamo popolare il nostro markup per mostrare l’elenco dei post:

pages/blog/index.vue

<ul v-if="posts && posts.length > 0" class="posts-list__wrapper">
  <li v-for="(post, i) in posts" :key="i" class="posts-list__item" :class="{ 'posts-list__item--first': i === 0 }">
    <a v-if="i === 0" href="#">
      <prismic-image v-if="post.data && post.data.image" :field="post.data.image" sizes="(max-width: 990px) 100vw (min-width: 991px) 57vw" />
    </a>
    <a v-else href="#">
      <prismic-image v-if="post.data && post.data.image" :field="post.data.image" sizes="(max-width: 990px) 100vw (min-width: 991px) 33vw" />
    </a>

    <div class="posts-list__item-meta">
      <h3 class="posts-list__item-title">
        <n-link
          :to="
            localePath({
              name: 'blog-post',
              params: {
                post: post.uid
              }
            })
          "
        >
          {{ post.data.title[0].text }}
        </n-link>
      </h3>
      <span class="posts-list__item-author">
        {{ $t('blog.post_of') }}
        <n-link
          v-if="post.data && post.data.author"
          :to="
            localePath({
              name: 'author',
              params: {
                name: post.data.author.uid
              }
            })
          "
          rel="author"
        >
          {{ post.data.author.slug.replace('-', ' ') }}
        </n-link>
      </span>
      <div class="posts-list__item-summary">
        <prismic-rich-text v-if="post.data && post.data.summary" :field="post.data.summary" />
      </div>
    </div>
  </li>
</ul>

In dettaglio: LINK AL COMMIT.

E siamo quasi giunti alla fine di questa escursione!

Vediamo in ultimo come mostrare il dettaglio di un custom type di tipo repeatable.

Modifichiamo pertanto la nostra pagina dinamica _post.vue sotto alla cartella “blog” alla quale passeremo come parametro post lo UID tratto da Prismic (vedi precedente blocco di codice).

pages/blog/_post.vue

async asyncData({ $prismic, params, error, app }) {
  const currentLocale = app.i18n.locales.filter((lang) => lang.code === app.i18n.locale)[0]
  const doc = await $prismic.api.getByUID('post', params.post, {
    lang: currentLocale.iso.toLowerCase()
  })

  if (doc) {
    return {
      post: doc.results || doc
    }
  } else {
    error({ statusCode: 404, message: 'Page not found' })
  }
},

Questo ci permette di vedere in azione un’altra delle query che ci mette a disposizione Prismic e cioè getByUID.

Brevemente il dettaglio del recupero dati nell’html:

pages/blog/_post.vue

<article class="post">
  <header class="post__header">
    <prismic-rich-text id="js-title-post" :field="post.data.title" />
  </header>

  <prismic-image v-if="post.data && post.data.image" :field="post.data.image" sizes="(max-width: 990px) 100vw (min-width: 991px) 57vw" />

  <div class="post__content">
    <prismic-rich-text :field="post.data.content" />
  </div>
</article>

In dettaglio: LINK AL COMMIT.

Et voilà!


Rimangono alcuni punti da affrontare che sicuramente i più attenti hanno notato:

  • Lo switcher della lingua nelle pagine post non tiene conto del fatto che non solo il codice lingua cambia ma anche lo UID
  • Manca la pagina dettaglio per utente con i post relativi

Sono tutti task risolvibili che però non affronterò in questo post perché è già estremamente lungo.

Volete provare voi a cimentarvi in queste sfide? 🙃

Articolo scritto da