Nicolas Leport - Blog personnel
Il ne peut plus rien nous arriver d'affreux maintenant
– Red is dead, La cité de la peur
  Temps de lecture estimé : 16 min
Un site performant et (presque) totalement gratuit ? C'est possible !

Un site performant et (presque) totalement gratuit ? C'est possible !

Derrière ce titre accrocheur, une réalité. Nous verrons ici comment conjuguer gratuité et performance sans négliger le tooling, le déploiement continu, la stabilité et la sécurité de l'ensemble. Let's goooo !

*Disclaimer : cet article ne mesurera pas l'impact énergétique de la solution ni la sobriété numérique de l'ensemble. Ce n'est pas une stack éco-responsable. Ce sujet fera l'objet de publications et d'expérimentations futures. Maintenant on peut commencer.*

Une stack pourtant pas nouvelle mais qui est à la mode depuis quelque temps, je suis sûr que vous avez deviné... je parle bien sûr de la JAMSTACK ! Derrière cet acronyme accrocheur, une signification : Javascript API Markup STACK. C'est ce dont nous allons parler dans cet article.

Résultat final

Le site en ligne : nkcreation.com

Et parce que je n'ai rien à cacher à part mes variables d'environnement, tout le code est ouvert et disponible aux adresse suivantes :

Le dépôt GitHub du site : nkCreation/nkcreation.com

Le dépôt GitHub du CMS : nkCreation/apinkcreation-directus

Un peu d'histoire

Ceux qui fréquente ce blog depuis un moment (et je sais que vous êtes nombreux, sisi) le savent : j'avais commencé un article sur les applications isomorphiques. Rien que pour le nom je trouvais ça énormément stylé. Entre temps l'eau a coulée et de nouvelles choses sont arrivées sur plus ou moins les mêmes stack et mes différentes missions m'ont amenées sur d'autres horizons technique et je n'ai jamais pris le temps de finir cet article pour la simple et bonne raison qu'il était un peu trop ambitieux, car la stack était plus compliquée...

De VueJS avec Nuxt et Directus je suis passé à React avec Gatsby et Strapi puis Netlify CMS (stack actuelle du blog) pour enfin revenir à Nuxt et Directus car 90% de mon temps professionnel aujourd'hui est rempli de VueJS.

Prérequis

  • Une connaissance de GIT, et une clé SSH paramétrée.
  • Un compte sur GitHub, Heroku, AWS et Netlify.
  • Un gestionnaire de mot de passe, c'est mieux qu'un bloc note.
  • Savoir lire l'anglais (et oui).
  • Avoir du temps devant soi.

Petit tour technique du propriétaire

Nous en avons déjà évoqué un bout dans le chapitre précédent mais en gros, voici ce que nous allons faire : un site web administrable grâce à un CMS. Rien de bien sorcier jusque là et pourtant, nous allons faire tout ce qu'il faut pour faire des trucs compliqué qui feront super bien une fois que vous mettrez tout ça sur votre CV 🤓.

Pour ça, nous allons développer notre site internet avec NuxtJS, qui le générera côté serveur pour créer des pages HTML totalement statiques - ce qui sera bénéfique pour le référencement - grâce à une API issue de Directus, un headless CMS écrit en NodeJS. Ce site sera hébergé sur Netlify, un CDN - vu que ce ne sera que des pages HTML classiques (enfin pas tout à fait, nous verrons après) - et le CMS sera lui hébergé sur Heroku avec des addons PostgreSQL et Redis. Les fichiers seront stockés sur un bucket S3 chez AWS. Le tout disponible en public sur GitHub et sa CI magique : les GitHub Actions.

Ça donne envie hein ?

Première étape : le CMS

Nous allons commencer avec le CMS, comme ça nous pourrons brancher directement la génération du site dessus quand nous le développerons.

Stack en local

Si vous souhaitez utiliser Directus en local vous devrez créer une base de données. Étant sur macOS je ne pourrai pas vous conseiller pour Windows mais pour macOS c'est très simple, il faut d'abord installer HomeBrew.

Base de données

Nous allons installer Postgres et créer l'utilisateur et sa base de données associée. Je vous conseille de créer un utilisateur par application et une base qui va avec.

brew install postgresql

Ensuite, pour le user et la base :

createuser [username] --createdb -P
createdb [databasename] -U [username]

Notez bien le mot de passe que vous choisissez et le nom de la base, vous en aurez besoin pour la suite.

Directus CMS

Pour créer une application Directus, c'est très simple il suffit d'exécuter la commande ci-dessous, en pensant à remplacer my-awesome-projet par le nom de votre projet. Pour plus d'informations n'hésitez pas à consulter le quickstart guide très bien fait sur leur site.

npx create-directus-project my-awesome-projet

Cet utilitaire va vous demander diverses informations, notamment celles sur la base de données qu'on aura créé juste au dessus.

Sans plus attendre, nous allons dès maintenant ajouter dans un fichier .gitignore les lignes suivantes :

.env
node_modules
uploads

Cela nous évitera de pousser par erreur le fichier d'environnement dans lequel il y a tous les secrets disponible au bon fonctionnement de l'application, ainsi que tous les fichiers que l'on uploadera dans le CMS sur notre machine.

Dépot GIT

Une fois fait, vous pouvez créer un dépôt sur GitHub en mode public et suivre les informations données dans la section push an existing folder. C'est sensiblement quelque chose comme ça :

git init
git branch -M main
git add .
git commit -m "Initial commit"
git remote add origin [your_github_url_or_SSH_repository_link]
git push -u origin main

Déploiement sur Heroku

Pourquoi Heroku ? et bien car le premier plan de tarification ne coûte rien et que ça suffira largement pour notre usage. Certes ce n'est pas le plus performant mais vous verrez ensuite que l'API ne sera pas utilisée par les visiteurs du site web mais uniquement au build, donc pas de soucis de performance de ce côté là...

Vous allez devoir créer une nouvelle application sur Heroku et choisir un petit nom mignon et la région qui va bien.

Une fois créée il faut la connecter avec GitHub et choisir le dépot précédemment créé. Vous pouvez choisir la branche à déployer et paramétrer le déploiement automatique au push, ce que je vous conseille de faire.

Les addons : base de données et cache.

Nous allons PostgreSQL sur notre application. Pour cela, rendez-vous dans l'onglet Resources de votre application sur Heroku et chercher l'addon Heroku Postgres. Ajoutez le. Puis l'addon Heroku Redis.

Ces addons ajouteront dans les variables d'environnement de votre application les informations de connexion à leur services. Ces informations pourront changer dans le temps et nous n'aurons pas besoin de modifier notre application pour que ça reste fonctionnel. Hourra.

Les variables d'environnement

Pour que notre application fonctionne, il va falloir dire à Directus ce qu'il faut faire. Afin de simplifier la chose nous allons ajouter dans notre application un fichier à la racine nommé directus.config.js. Ce fichier va faire du mapping entre les variables du PATH et les valeur que Directus veut lire.

module.exports = {
  DB_CONNECTION_STRING:
    (process.env.DB_CONNECTION_STRING || process.env.DATABASE_URL) +
    "?ssl=true&sslmode=no-verify",
  ACCESS_TOKEN_TTL: process.env.ACCESS_TOKEN_TTL,
  ADMIN_EMAIL: process.env.ADMIN_EMAIL,
  ADMIN_PASSWORD: process.env.ADMIN_PASSWORD,
  CACHE_REDIS: process.env.CACHE_REDIS || process.env.REDIS_URL,
  CACHE_ENABLED: process.env.CACHE_ENABLED,
  CACHE_NAMESPACE: process.env.CACHE_NAMESPACE,
  CACHE_AUTO_PURGE: process.env.CACHE_AUTO_PURGE,
  CACHE_STORE: process.env.CACHE_STORE,
  CONFIG_PATH: process.env.CONFIG_PATH,
  DB_CLIENT: process.env.DB_CLIENT,
  EXTENSIONS_PATH: process.env.EXTENSIONS_PATH,
  NODE_ENV: process.env.NODE_ENV,
  PUBLIC_URL: process.env.PUBLIC_URL,
  RATE_LIMITER_REDIS: process.env.RATE_LIMITER_REDIS || process.env.REDIS_URL,
  RATE_LIMITER_DURATION: process.env.RATE_LIMITER_DURATION,
  RATE_LIMITER_ENABLED: process.env.RATE_LIMITER_ENABLED,
  RATE_LIMITER_KEY_PREFIX: process.env.RATE_LIMITER_KEY_PREFIX,
  RATE_LIMITER_POINTS: process.env.RATE_LIMITER_POINTS,
  RATE_LIMITER_STORE: process.env.RATE_LIMITER_STORE,
  REFRESH_TOKEN_COOKIE_NAME: process.env.REFRESH_TOKEN_COOKIE_NAME,
  REFRESH_TOKEN_COOKIE_SAME_SITE: process.env.REFRESH_TOKEN_COOKIE_SAME_SITE,
  REFRESH_TOKEN_TTL: process.env.REFRESH_TOKEN_TTL,
  STORAGE_LOCATIONS: process.env.STORAGE_LOCATIONS,
  STORAGE_S3_BUCKET: process.env.STORAGE_S3_BUCKET,
  STORAGE_S3_DRIVER: process.env.STORAGE_S3_DRIVER,
  STORAGE_S3_ENDPOINT: process.env.STORAGE_S3_ENDPOINT,
  STORAGE_S3_REGION: process.env.STORAGE_S3_REGION,
  TZ: process.env.TZ,
}

Le petit trick ici est le ?ssl=true&sslmode=no-verify à la fin de l'URL, nécessaire pour que Heroku puisse se connecter à la BDD Postgres.

Si jamais il vous manque une variable d'environnement, n'oubliez pas de l'ajouter dans ce fichier après l'avoir ajoutée dans Heroku. Vous ne pourrez pas dire que vous ne le saviez pas.

Maintenant, il faut remplir tout ça 😱 direction les Settings de notre application Heroku, puis dans Config Vars cliquer sur Reveal Config Vars. Vous noterez que certaines sont présentes, n'y touchez pas.

Certaines sont très importantes :

  • ADMIN_EMAIL sera l'email de connexion à Directus du compte Admin.
  • ADMIN_PASSWORD ai-je vraiment besoin d'expliquer ?
  • CONFIG_PATH le lien vers le fichier JS qu'on vient de créer pour que Directus puisse lire notre configuration.
  • DB_CLIENT à bien mettre à pg sinon vous vous demanderez pourquoi rien ne marche.

Voici une partie de la configuration que j'ai mise, n'hésitez pas à modifier les valeurs, tout est expliqué dans la doc de Directus.

ACCESS_TOKEN_TTL                              20m
ADMIN_EMAIL                                   [your_email]
ADMIN_PASSWORD                                [password]
CACHE_AUTO_PURGE                              true
CACHE_ENABLED                                 true
CACHE_NAMESPACE                               cache
CACHE_STORE                                   redis
CONFIG_PATH                                   /app/directus.config.js
DB_CLIENT                                     pg
EXTENSIONS_PATH                               /app/extensions
KEY                                           [key, mettez celle de votre application locale dans le fichier .env]
NODE_ENV                                      production
PGSSLMODE                                     no-verify
PUBLIC_URL                                    /
RATE_LIMITER_DURATION                         1
RATE_LIMITER_ENABLED                          true
RATE_LIMITER_KEY_PREFIX                       rate-limitter
RATE_LIMITER_POINTS                           30
RATE_LIMITER_STORE                            redis
REFRESH_TOKEN_COOKIE_NAME                     directus_refresh_token
REFRESH_TOKEN_COOKIE_SAME_SITE                lax
REFRESH_TOKEN_TTL                             7d
SECRET                                        [idem que la KEY]
STORAGE_LOCATIONS                             s3
STORAGE_S3_BUCKET                             [nom du bucket S3, on verra plus tard]
STORAGE_S3_DRIVER                             s3
STORAGE_S3_ENDPOINT                           s3.amazonaws.com
STORAGE_S3_KEY                                [nous verrons dans la section S3]
STORAGE_S3_REGION                             [region de votre bucket]
STORAGE_S3_SECRET                             [nous verrons dans la section S3]
TZ                                            Europe/Paris

Le démarrage de l'application

Nous allons ensuite devoir créer ce qu'Heroku appelle des Dynos pour lancer l'application. Pour ce faire, ajoutez un fichier nommé Procfile sans extension à la racine de votre application et mettez dedans :

release: npx directus bootstrap
web: npx directus start

La première ligne servira lors des mises à jour de Directus, elle jouera les migrations de la base de données si le schéma change dans le core de Directus. La deuxième permet de lancer l'application, c'est aussi simple que ça.

Le stockage des fichiers sur AWS

Prochaine et dernière étape pour la partie CMS, le stockage des fichiers uploadés. En l'état l'application peut et doit déjà pouvoir se lancer mais vous ne pourrez pas télécharger des fichiers dans le CMS.

Création du bucket

Pour ce faire, connectez-vous sur AWS et rendez-vous dans l'interface de gestion des buckets. Créer un bucket - compartiment en français - et choisissez lui un petit nom. Pas besoin de mettre l'accès aux objets en publique, vous pouvez laisser les réglages tels qu'ils sont.

Pensez à remplir dans Heroku - Settings de votre application les variables STORAGE_S3_BUCKET et STORAGE_S3_REGION par ce que vous avez choisi.

Clé d'accès pour AWS

Notre bucket est créé, nous allons générer une clé d'accès pour que Directus puisse streamer les fichiers depuis le S3. Rendez-vous sur la page des clés d'accès dans l'IAM AWS et créez une clé pour Directus.

Notez bien les informations et reportez les dans les variables d'environnement associées STORAGE_S3_KEY et STORAGE_S3_SECRET.

Votre CMS est maintenant prêt et vous pouvez y accéder via l'URL fournie par Heroku. Bravo !

Deuxième étape : le site web.

Nous allons maintenant créer le site web qui utilisera cette API. Je ne rentrerai pas dans les détails de comment créer un site web ou comment paramétrer vos pages, je vous donnerai simplement un exemple de comment est configuré mon projet pour que vous puissiez vous inspirer. L'important ici est le branchement de notre API dans Nuxt !

Création du projet Nuxt

Placez-vous dans votre répertoire de travail et lancer la commande npm init nuxt-app [your-nuxt-app-name]. Il vous demandera quelques questions concernant vos préférences de développement, voici ce que j'aurai tendance à choisir par défaut :

✨  Generating Nuxt.js project in your-nuxt-app-name
? Project name: your-nuxt-app-name
? Programming language: JavaScript
? Package manager: Npm
? UI framework: None
? Nuxt.js modules: Axios - Promise based HTTP client, Progressive Web App (PWA)
? Linting tools: ESLint, Prettier
? Testing framework: Jest
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Static (Static/Jamstack hosting)
? Development tools: jsconfig.json (Recommended for VS Code if you're not using typescript)
? Continuous integration: GitHub Actions (GitHub only)
? What is your GitHub username? your-github-username
? Version control system: Git

L'important ici est de bien choisir "Static" dans le deployment target pour utiliser Netlify CDN ensuite. Vous remarquerez si vous allez dans le dépôt de mon site que j'ai choisi Typescript. C'est historique et je ne recommande pas spécialement d'utiliser TS avec Vue 2 (et Nuxt 2).

Si vous avez un doute, n'hésitez pas à aller voir la doc de Nuxt, très bien faite, à cette adresse.

Une fois fait, vous pouvez suivre les informations qui sont output dans votre terminal et lancer votre appli ! 🤘

Ajout de l'API Directus à la configuration

Maintenant que votre appli fonctionne nous allons brancher tout ça. Premièrement, créez un fichier .env s'il n'existe pas déjà dans votre répertoire et ajoutez-y une ligne concernant votre url d'API. Ensuite reportez cette valeur dans le nuxt.config.js. Par exemple :

# file .env

API_URL="https://[your-api-name-on-heroku].herokuapp.com/"
// file nuxt.config.js

export default {
  // env object : add this on top of the exported object.
  env: {
    apiUrl: process.env.API_URL,
  },
  // [...]
}

Prenez l'habitude de faire ça pour tout ce qui est spécifique à votre projet (URL API, Token, API KEY, etc...). Cela permet d'éviter de mettre des secrets en clair dans votre dépôt GitHub et surtout de réutiliser l'application branchée sur une autre URL d'API par exemple si vous avez plusieurs environnements (prod, preprod, staging, int...)

BONUS : Création d'une collection sur Directus

Un exemple d'utilisation de votre API pourrait être de générer des pages à la volée (comme pour un blog par exemple) avec un slug. Je ferai certainement un article dédié à ça plus tard, mais pour le moment on va faire quelque chose de simple : une collection singleton qui permettra de modifier un message sur la page d'accueil du site. Simple, efficace, utile, pratique.

  1. Connectez-vous à votre admin et allez dans la section Settings à gauche (la roue crantée) puis dans Data Model et créez un nouvel item, que j'appellerai ici Hero.
  2. Cochez la case Treat as single object dans la section Singleton.
  3. Choisissez par un Primary Key Field id de type Generated UUID. Cela évitera que l'API soit prévisible si quelqu'un en vient à trouver l'URL (/1, /2, etc...)
  4. Ajoutez un champ title de type Text en cliquant sur + Create Field. Vous pouvez choisir dans Interface l'option Textarea, cela permettra de gérer des retours à la ligne par exemple (qu'on transformera avec une regex dans le code).

🎉 Vous avez créé une collection ! Maintenant, il faut la rendre visible. Pour ça direction les Settings toujours mais cette fois dans Roles & Permissions puis dans Public. Ici il faut cocher l'oeil dans votre collection pour rendre accessible via l'API vos données.

Vous pouvez également cliquer sur System Collections et ensuite rendre visible via l'oeil la section Directus Files. Sinon les photos uploadées dans vos collections ne seront pas accessible (et donc pas intégrable dans une balise <img /> par exemple).

Il ne vous reste plus qu'à remplir votre champ title.

Utilisation de l'API dans l'app

Directus est un super projet et en plus de fournir une API Rest il fourni aussi une API GraphQL. Nous allons l'utiliser ici car vous verrez par la suite que si vous requêtez plusieurs collections ça va devenir verbeux. Pour ça nous allons utiliser nuxt-graphql-request, qui va nous permettre d'utiliser des Query GraphQL dans notre application.

Si vous ne connaissez pas GraphQL, n'hésitez pas à vous rendre sur le site How to GraphQL qui est très bien fait et permet de prendre en main rapidement et de comprendre l'intérêt de l'outils. Et si vous souhaitez en savoir toujours plus, vous pouvez aussi participer à une formation GraphQL donnée par moi-même ou mes collègues de Zenika. #instantpub

npm install nuxt-graphql-request graphql --save-dev
npm install --save graphql-tag

Et ensuite on ajoute la configuration nécessaire dans le nuxt.config.js à savoir une entrée nuxt-graphql-request dans le tableau buildModules et la configuration GraphQL nécessaire au fonctionnement :

export default {
  // [...]
  // Modules for dev and build (recommended) (https://go.nuxtjs.dev/config-modules)
  buildModules: [
    'nuxt-graphql-request',
    // [...]
  ],
  graphql: {
    clients: {
      default: {
        endpoint: process.env.API_URL + 'graphql',
      },
    },
  },
  // [...]
}

Remarquez ici l'utilisation encore une fois de notre variable d'environnement.

Maintenant il ne nous reste plus qu'à faire une requête et afficher le résultat ! Rendez-vous dans le fichier pages/index.vue et collez-y le code suivant.

<template>
  <div>
    <h1 v-html="title"></h1>
  </div>
</template>

<script>
import { gql } from 'graphql-tag'

export default {
  async asyncData({ $graphql }) {
    const query = gql`
      query homeData {
        hero {
          title
        }
      }
    `

    const { hero } = await $graphql.default.request(query)

    return {
      title: hero.title.replace(/\n/g, '<br />'),
    }
  },
}
</script>

Décomposons un peu tout ça.

La partie <template> est assez simple, elle affiche dans un <h1> du HTML provenant de la variable title de notre composant Vue.

La partie <script> est notre composant Vue, ici chargée en tant que Page d-Nuxt. Afin de charger les données côté serveur nous utilisons un hook qui ne sera pas appelé côté client et qui permet de faire quelque chose de vraiment complètement statique une fois le build fait. Ce hook est asyncData qui est - comme son nom l'indique - asynchrone.

Ce hook met à disposition en paramètre un contexte contenant diverses choses. Ici, je décompose le paramètre avec la syntaxe de décomposition et ne récupère que l'objet $graphql qui se trouve dans l'objet de contexte.

Nous créons ensuite dans cette méthode une query GraphQL en appelant la méthode gql importée du paquet correspondant. Ici c'est de la syntaxe GraphQL de base et on indique à notre API vouloir récupérer le champ title de notre collection hero.

Si vous avez appelez votre collection différemment n'oubliez pas de changer le nom de la collection appelée. C'est ici tout l'intérêt de GraphQL, ne sélectionner que les champs voulus et utiles pour notre application.

Nous appelons notre api avec en paramètre notre query en décomposant la sortie (qui a la même structure que l'entrée) puis nous retournons les valeur souhaitées en fin de fonction. Ces valeurs seront fusionnées avec celles de la propriété data du composant Vue. La petite astuce ici consiste à remplacer les \n de notre champ de type textarea par des <br /> afin de mettre des retours à la ligne valides en HTML.

Sommaire