J'ai commencé ce blog, Grav, en 2016. C'était une façon pratique, sympathique et facile de mettre les choses en place.

J'ai pris le theme LingonBerry et seulement changé quelques lignes du code CSS et quelques couleurs. Cela à bien fonctionné pendant des années.

Récemment j'ai voulu revenir sur mon blog et ajouter du contenu, j'ai regardé le code qui était en place. Le theme n'utilise que du CSS à plat ainsi que du JS pour son style et son interaction avec les utilisateurs.

C'est une base sympa mais, depuis les années, je me suis habitué à TypeScript et SCSS. Cela rend le développement plus rapide et sécurisé mais le navigateur ne comprend pas ce code en dur, il faut donc trouver un moyen de lui traduire tout ça.

Installer WebPack Encore

Je me suis habitué à WebPack Encore avec mes projets Symfony et il avait tendance à s'occuper de tout. Pour voir comment implémenter cela dans un projet Grav, j'ai lu la Documentation et installé webpack encore avec yarn :

yarn add @symfony/webpack-encore --dev

D'habitude quand je l'installe dans un projet symfony avec Flex il génère les fichiers de configuration de base. Ici ce n'est pas le cas donc j'ai créé mon propre webpack.config.js à la racine de mon projet :

const Encore = require('@symfony/webpack-encore');
const path = require('path');
const CompressionPlugin = require('compression-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

// Manually configure the runtime environment if not already configured yet by the "encore" command.
// It's useful when you use tools that rely on webpack.config.js file.
if (!Encore.isRuntimeEnvironmentConfigured()) {
        Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}

const PATH = path.join('user', 'themes', 'lingonberry-custom')
const BUILD_PATH = path.join(PATH, 'build')

Encore
        // directory where compiled assets will be stored
        .setOutputPath(BUILD_PATH)
        // public path used by the web server to access the output path
        .setPublicPath(path.join('/', BUILD_PATH))

        .addEntry('app', path.join('./', PATH, 'js', 'app.ts'))

        .disableSingleRuntimeChunk()

        .cleanupOutputBeforeBuild()

        .enableSourceMaps(!Encore.isProduction())
        // enables hashed filenames (e.g. app.abc123.css)
        .enableVersioning(Encore.isProduction())

        .addPlugin(new CompressionPlugin())
        // .addPlugin(new MiniCssExtractPlugin())

        .addPlugin(new HtmlWebpackPlugin({
                inject: false,
                filename: 'assets.html',
                publicPath: path.join('/', BUILD_PATH),
                scriptLoading: 'defer',
                templateContent: ({htmlWebpackPlugin}) => `
        ${htmlWebpackPlugin.tags.headTags}
        `
        }))

        // enables @babel/preset-env polyfills
        .configureBabelPresetEnv((config) => {
                config.useBuiltIns = 'usage';
                config.corejs = 3;
        })

        .enableSassLoader()
        .enableTypeScriptLoader()

module.exports = Encore.getWebpackConfig();

J'ai rajouté quelques plugins pour la compilation du SASS et du TS et quelque uns d'autres que nous verrons plus tard. Ensuite j'ai rajouté des variables :

const PATH = path.join('user', 'themes', 'lingonberry-custom')
const BUILD_PATH = path.join(PATH, 'build')

Le theme de mon site se trouve dans le répertoire user/themes/lingonberry-custom, J'ai donc voulu mettre tout ça dans une variable pour pouvoir changer de thème un jour sans avoir à tout réécrire dans mon fichier de configuration WebPack.

Il faut dire à WebPack quels fichiers regarder et où les mettre une fois traduits. Avec Grav le 'outputPath' et 'publicPath' sont pratiquement au même endroit car ils partagent leur racine de répertoire (tous les fichiers servis par le serveur front sont dans le même répertoire que tous les fichiers php qui génèrent le back).

        .setOutputPath(BUILD_PATH)
        .setPublicPath(path.join('/', BUILD_PATH))
        .addEntry('app', path.join('./', PATH, 'js', 'app.ts'))

J'ai rajouté un fichier app.ts dans mon répertoireuser/themes/lingonberry-custom/js/ qui arrivera en tant queuser/themes/lingonberry-custom/build/app.js

Construire les assets

Une fois la configuration principale mise en place il faut trouver un moyen de regarder nos fichiers et reconstruire les traductions à chaque modification. Au premier lancement de la commande yarn run un fichier package.json à du être généré à la racine du projet :

{
  "devDependencies": {
    "@symfony/webpack-encore": "^1.1.2"
  }
}

Cela ne suffira pas pour tout dont nous allons avoir besoin. J'ai rajouté des devDependencies pour toute la traduction SCCS et TypeScript ainsi que quelques scripts pour faciliter le lancement des scripts dont nous allons avoir besoin pour les construtcions des fichiers :

{
  "devDependencies": {
    "@symfony/webpack-encore": "^1.1.2",
    "@types/highlightjs": "^10.1.0",
    "compression-webpack-plugin": "^7.1.2",
    "core-js": "^3.9.1",
    "file-loader": "^6.0.0",
    "html-webpack-plugin": "^5.3.1",
    "sass": "^1.32.8",
    "sass-loader": "^11.0.0",
    "ts-loader": "^8.0.1",
    "typescript": "^4.2.3"
  },
  "scripts": {
    "dev-server": "encore dev-server",
    "dev": "encore dev",
    "watch": "encore dev --watch",
    "build": "encore production --progress"
  }
}

Si vous mettez à jour votre fichier package.json lancez la commande yarn install pour récupérer les nouvelles dépendances sinon le build va échouer. Si vous avez yarn en local vous pouvez lancer la commande yarn watch dans votre environnement de développement et à chaque fois que vous modifiez un des fichiers les traductions seront reconstruites automatiquement. J'ai tendance à faire passer mon environnement de développement par des containers docker définis dans un fichier docker-compose.yaml à la racine de mon projet. Voici ce dont l y a besoin pour lui faire exécuter les fonctions que nous venons de définir :

version: '3.7'

services:
  node:
    image: node:14-alpine
    user: node
    working_dir:
      /srv/app/public
    volumes:
      - ./:/srv/app/public
    command: >
      sh -c "cd /srv/app/public
      && yarn
      && yarn watch"

Avec la commande docker-compose up -d il vérifiera vos fichiers et les reconstruira à chaque modification.

Ajouter du SCSS

Il y a un fichier TypeScript qui est automatiquement traduit en JS mais je veux aussi passer du SCSS en CSS. Cela est plus tôt simple, avec l'ajout de notre plugin .enableSassLoader() sur le webpack.config.js. Je peux maintenant ajouter un fichier dans user/lingonberry-custom/css aux nom de custom.scss et un autre appelé _variables.scss :

//_variables.scss
$bg-color: #f1f1f1;
$primary-color: #ff6a1e;

//custom.scss
@use 'variables' as *;

body {
  a {
    color: $primary-color;
    }
}

Pour faire en sorte que WebPack s'en occupe il faut importer ces fichiers dans le app.ts :

import '../css/custom.scss'

Cela va générer un fichier app.cd dans le répertoire build du theme.

Importer les fichiers à l'intérieur du thème

C'est une des parties les plus compliquées que j'ai rencontrées, J'étais habitué à utiliser twig pur le WebPack Encore de la manière suivante :

{% block stylesheets %}
    {{ encore_entry_link_tags('app') }}
{% endblock %}

{% block javascripts %}
    {{ encore_entry_script_tags('app') }}
{% endblock %}

Ici cela n'est pas possible, Je me suis tourné vers le HtmlWebpackPlugin pour générer un fichier HTML qui contient tous les link et script dont je vais avoir besoin dans mon thème. C'est pour cela que j'ai rajouté une partie dans mon webpack.config.js :

        .addPlugin(new HtmlWebpackPlugin({
                inject: false,
                filename: 'assets.html',
                publicPath: path.join('/', BUILD_PATH),
                scriptLoading: 'defer',
                templateContent: ({htmlWebpackPlugin}) => `
        ${htmlWebpackPlugin.tags.headTags}
        `

Cela à l'air plus compliqué que le reste, et, ça l'est. Par défaut le plugin HtmlWebpackPlugin va générer un fichier HTML basic avec des tags comme html, body, title, ... Mon thème de base contient déja ces balises et je veux trouver un moyen de n'ajouter que le JS et le CSS traduit par mon WebPack. C'est pour cela que j'ai modifié les options suivantes ;

  • inject : false // Je ne veux pas injecter du HTML dans un fichier déjà existant, je ne veux que les balises des scripts et des links dont je vais avoir besoin.
  • filename : 'assets.html' // Définir le nom du fichier qui va être généré.
  • publicPath : path.join('/', BUILD_PATH) // Dire à WebPack quel sera le lien du domaine publique pour les fichiers.
  • scriptLoading : 'defer' // Afin de faciliter l'importation des fichiers je veux qu'un seul fichier. Pour cela je passe le JavaScript en defer, il attendra la fin du chargement de la page avant de se lancer.
  • templateContent : ({htmlWebpackPlugin}) => ${htmlWebpackPlugin.tags.headTags} })) // Cela permet au plugin de comprendre comment générer son fichier HTML. Par défaut il modifie le contenu d'un fichier existant. Ici je veux un fichier qui contient que le minimum afin de pouvoir l'incorporer dans la base de mon template.

Avec tout ça quand je lance la commande de build un fichier HTML sera générée dans le répertoire build de mon thème :

<!-- user/themes/lingonberry-custom/build/assets.html -->
 <script defer src="/user/themes/lingonberry-custom/build/app.js"></script><link href="/user/themes/lingonberry-custom/build/app.css" rel="stylesheet">

On se rapproche de la fin. Il reste à trouver une façon d'incorporer ce fichier généré à l'intérieur de la base de notre thème. Par défaut Twig ne va pas du tout regarder dans le dossier build que nous venons de générer. J'ai donc modifié les fichiers PHP de base de mon thème afin d'inclure ce dossier dans la base Twig en modifiant le fichier lingonberry-custom.php à la racine de mon thème :

<?php
namespace Grav\Theme;

use Grav\Common\Theme;
use Grav\Common\Grav;

class LingonberryCustom extends Theme
{
    public static function getSubscribedEvents() {
        return [
            'onTwigLoader' => ['onTwigLoader', 10]
        ];
    }

    public function onTwigLoader() {
        $themePath = Grav::instance()['locator']->findResource('themes://lingonberry-custom');
        $this->grav['twig']->addPath($themePath . DIRECTORY_SEPARATOR . 'build');
    }
}

Maintenant que Twig sait qu'il faut surveiller ce répertoire il est facile d'intégrer ce fichier dans notre template de base :

<head>
    {% include 'assets.html' %}
</head>

C'est génial, tout fonctionne en local. Maintenant il va falloir optimiser les traductions de nos fichiers pour le serveur de production. Si vous construisez les fichiers en local avec la commande yarn encore production et que vous les envoyez vers votre serveur de production cela devrait fonctionner sans trop de soucis mais va créer des fichiers inutiles. À chaque traduction des fichiers TS et CSS le fichier de sortie aura un nom différent et donc ça sera un nouveau fichier qui sera exporté vers votre serveur. Les noms des fichiers changent afin de pouvoir optimiser la configuration des fichiers cache de votre serveur front. Cela veux dire que si quelqu'un revient sur votre site mais votre code JS ou CSS n'a pas changé depuis la dernière fois qu'il est venu il n'aura pas besoin d'aller le télécharger. Bientôt je publierai un article sur les configurations de policies de cache nGinx. Du coup à chaque 'build' en mode production le fichier aura un nom différent, comme app.400c9c83.js.

C'est pour cela que j'ai tendance à lancer des builds sur mon GitLab privé qui finit par faire un rsync --delete sur le serveur de production. Si jamais vous voulez en savoir plus sur le côté CI/CD dîtes le moi et je publierai un truc détaillé dessus ;)