<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Guillaume</title>
    <description>The latest articles on DEV Community by Guillaume (@gbtux).</description>
    <link>https://dev.to/gbtux</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F616613%2F130bed53-3a0a-4853-b56e-3f8fafa2047a.png</url>
      <title>DEV Community: Guillaume</title>
      <link>https://dev.to/gbtux</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gbtux"/>
    <language>en</language>
    <item>
      <title>Symfony et Shadcn/ui</title>
      <dc:creator>Guillaume</dc:creator>
      <pubDate>Sat, 02 Aug 2025 07:36:02 +0000</pubDate>
      <link>https://dev.to/gbtux/symfony-et-shadcnui-4g2c</link>
      <guid>https://dev.to/gbtux/symfony-et-shadcnui-4g2c</guid>
      <description>&lt;p&gt;Hi guys !&lt;/p&gt;

&lt;p&gt;J'espère que vous avez tous vu les nouveaux kits de démarrage de Laravel avec Inertia, React et Shadcn/UI. C'est beau...compliqué mais beau.&lt;/p&gt;

&lt;p&gt;Suite à mon précédent article &lt;a href="https://dev.to/gbtux/react-dans-symfony-avec-vite-5e74"&gt;React dans Symfony avec Vite&lt;/a&gt; je me suis demandé pour les besoin d'un projet comment faire un front en React avec Shadcn/UI et un backend Symfony.&lt;/p&gt;

&lt;p&gt;Quelques précisions : ici on va utiliser une structure "Fullstack" (tout dans le même projet) alors qu'on pourrait coder un front en React isolé et juste le backend en Symfony. Pour des raisons de simplicité, notamment pour la gestion de l'authentification, c'est beaucoup plus simple de faire comme ça. Mais au final, notre front et notre back vont bien communiquer via des API REST comme si ils étaient isolés.&lt;/p&gt;

&lt;p&gt;Allez c'est parti !&lt;/p&gt;

&lt;h2&gt;
  
  
  Création du projet et ajout de React
&lt;/h2&gt;

&lt;p&gt;Je ne vais pas refaire le tuto. Créez votre projet comme indiqué dans mon précédent tuto &lt;a href="https://dev.to/gbtux/react-dans-symfony-avec-vite-5e74"&gt;React dans Symfony avec Vite&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vérifiez bien que React marche.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation de Tailwind
&lt;/h2&gt;

&lt;p&gt;On va installer Tailwind et son plugin Vite.&lt;/p&gt;

&lt;p&gt;A la racine de votre projet :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;tailwindcss @tailwindcss/vite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Puis ouvrez le fichier vite.config.js et configurez-le comme suit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;symfonyPlugin&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vite-plugin-symfony&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;react&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vitejs/plugin-react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;tailwindcss&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@tailwindcss/vite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nf"&gt;react&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nf"&gt;tailwindcss&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nf"&gt;symfonyPlugin&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;rollupOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./assets/main.jsx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./assets&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a ajouté :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;L'import du plugin Tailwind ligne 5&lt;/li&gt;
&lt;li&gt;le plugin dans la liste des plugins&lt;/li&gt;
&lt;li&gt;On a ajouté l'alias '@' qu'on définit à la racine de notre répertoire assets (important pour la suite sinon Shadcn ne sait pas où mettre ses composants)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Puis ajoutez la ligne suivante dans assets/styles/app.css:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ajoutez 2/ 3 classes dans votre App.jsx pour vérifier que ça marche bien après avoir relancé un "npm run dev"&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation de Shadcn/UI
&lt;/h2&gt;

&lt;p&gt;On installe tous les pré-requis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;class-variance-authority clsx tailwind-merge lucide-react tw-animate-css
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et là... la &lt;a href="https://ui.shadcn.com/docs/installation/manual" rel="noopener noreferrer"&gt;doc&lt;/a&gt; n'est pas claire et il y a une incohérence.&lt;/p&gt;

&lt;p&gt;Celle-ci ne fait mention que du fichier tsconfig.json, or celui-ci appelle 2 autres fichiers tsconfig.app.json et tsconfig.node.json (faut lire hein, c'est marqué dedans !).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note : Pour aller récupérer les 2 fichiers absents de la documentation, j'ai créé un projet Vite bidon (avec npm create vite@latest my-project) &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Créons maintenant nos 3 fichiers à la racine de notre projet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;tsconfig.json&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"references"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./tsconfig.app.json"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./tsconfig.node.json"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"baseUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"paths"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"@/*"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"./assets/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;tsconfig.app.json&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tsBuildInfoFile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./node_modules/.tmp/tsconfig.app.tsbuildinfo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"useDefineForClassFields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lib"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DOM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DOM.Iterable"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ESNext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"skipLibCheck"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="err"&gt;/*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Bundler&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;mode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*/&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moduleResolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bundler"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allowImportingTsExtensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"verbatimModuleSyntax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moduleDetection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"force"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noEmit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"jsx"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"react-jsx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="err"&gt;/*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Linting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*/&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noUnusedLocals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noUnusedParameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"erasableSyntaxOnly"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noFallthroughCasesInSwitch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noUncheckedSideEffectImports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"assets"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;tsconfig.node.json&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tsBuildInfoFile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./node_modules/.tmp/tsconfig.node.tsbuildinfo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES2023"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lib"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ES2023"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ESNext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"skipLibCheck"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="err"&gt;/*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Bundler&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;mode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*/&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moduleResolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bundler"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allowImportingTsExtensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"verbatimModuleSyntax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moduleDetection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"force"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noEmit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="err"&gt;/*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Linting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*/&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noUnusedLocals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noUnusedParameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"erasableSyntaxOnly"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noFallthroughCasesInSwitch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noUncheckedSideEffectImports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"vite.config.ts"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note : je n'ai rien modifié (simplement copié du projet Vite exemple) sauf la dernière ligne "include": ["assets"] de tsconfig.app.json que j'ai modifié de 'src' à 'assets' pour coller à nos répertoires Symfony.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Mais c'est pas fini !&lt;/p&gt;

&lt;p&gt;On va ajouter les styles Shadcn à notre CSS.&lt;/p&gt;

&lt;p&gt;Ouvrez le fichier assets/styles/app.css et ajouter les lignes comme suit (nous ne devriez avoir que la première ligne déjà présente):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tw-animate-css"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@custom-variant&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;is&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;dark&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.145&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--card-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.145&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--popover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--popover-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.145&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.205&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--primary-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.985&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--secondary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.97&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--secondary-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.205&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--muted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.97&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--muted-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.556&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.97&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--accent-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.205&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--destructive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.577&lt;/span&gt; &lt;span class="m"&gt;0.245&lt;/span&gt; &lt;span class="m"&gt;27.325&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--destructive-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.577&lt;/span&gt; &lt;span class="m"&gt;0.245&lt;/span&gt; &lt;span class="m"&gt;27.325&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.922&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.922&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--ring&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.708&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--chart-1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.646&lt;/span&gt; &lt;span class="m"&gt;0.222&lt;/span&gt; &lt;span class="m"&gt;41.116&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--chart-2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.6&lt;/span&gt; &lt;span class="m"&gt;0.118&lt;/span&gt; &lt;span class="m"&gt;184.704&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--chart-3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.398&lt;/span&gt; &lt;span class="m"&gt;0.07&lt;/span&gt; &lt;span class="m"&gt;227.392&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--chart-4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.828&lt;/span&gt; &lt;span class="m"&gt;0.189&lt;/span&gt; &lt;span class="m"&gt;84.429&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--chart-5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.769&lt;/span&gt; &lt;span class="m"&gt;0.188&lt;/span&gt; &lt;span class="m"&gt;70.08&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.625rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--sidebar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.985&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--sidebar-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.145&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--sidebar-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.205&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--sidebar-primary-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.985&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--sidebar-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.97&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--sidebar-accent-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.205&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--sidebar-border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.922&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--sidebar-ring&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.708&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.dark&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.145&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.985&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.145&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--card-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.985&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--popover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.145&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--popover-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.985&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.985&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--primary-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.205&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--secondary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.269&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--secondary-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.985&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--muted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.269&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--muted-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.708&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.269&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--accent-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.985&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--destructive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.396&lt;/span&gt; &lt;span class="m"&gt;0.141&lt;/span&gt; &lt;span class="m"&gt;25.723&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--destructive-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.637&lt;/span&gt; &lt;span class="m"&gt;0.237&lt;/span&gt; &lt;span class="m"&gt;25.331&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.269&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.269&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--ring&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.439&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--chart-1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.488&lt;/span&gt; &lt;span class="m"&gt;0.243&lt;/span&gt; &lt;span class="m"&gt;264.376&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--chart-2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.696&lt;/span&gt; &lt;span class="m"&gt;0.17&lt;/span&gt; &lt;span class="m"&gt;162.48&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--chart-3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.769&lt;/span&gt; &lt;span class="m"&gt;0.188&lt;/span&gt; &lt;span class="m"&gt;70.08&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--chart-4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.627&lt;/span&gt; &lt;span class="m"&gt;0.265&lt;/span&gt; &lt;span class="m"&gt;303.9&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--chart-5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.645&lt;/span&gt; &lt;span class="m"&gt;0.246&lt;/span&gt; &lt;span class="m"&gt;16.439&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--sidebar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.205&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--sidebar-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.985&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--sidebar-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.488&lt;/span&gt; &lt;span class="m"&gt;0.243&lt;/span&gt; &lt;span class="m"&gt;264.376&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--sidebar-primary-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.985&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--sidebar-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.269&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--sidebar-accent-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.985&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--sidebar-border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.269&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--sidebar-ring&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.439&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="nb"&gt;inline&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--background&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--foreground&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--card&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-card-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--card-foreground&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-popover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--popover&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-popover-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--popover-foreground&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--primary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-primary-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--primary-foreground&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-secondary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--secondary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-secondary-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--secondary-foreground&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-muted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--muted&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-muted-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--muted-foreground&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--accent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-accent-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--accent-foreground&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-destructive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--destructive&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-destructive-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--destructive-foreground&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--border&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-ring&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--ring&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-chart-1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--chart-1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-chart-2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--chart-2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-chart-3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--chart-3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-chart-4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--chart-4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-chart-5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--chart-5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--radius-sm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--radius&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--radius-md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--radius&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--radius-lg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--radius&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--radius-xl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--radius&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-sidebar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--sidebar&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-sidebar-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--sidebar-foreground&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-sidebar-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--sidebar-primary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-sidebar-primary-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--sidebar-primary-foreground&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-sidebar-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--sidebar-accent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-sidebar-accent-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--sidebar-accent-foreground&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-sidebar-border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--sidebar-border&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-sidebar-ring&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--sidebar-ring&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@layer&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;@apply&lt;/span&gt; &lt;span class="err"&gt;border-border&lt;/span&gt; &lt;span class="err"&gt;outline-ring/50;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;@apply&lt;/span&gt; &lt;span class="err"&gt;bg-background&lt;/span&gt; &lt;span class="err"&gt;text-foreground;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Créez un fichier assets/lib/utils.ts tel que:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;clsx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ClassValue&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clsx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;twMerge&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tailwind-merge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cn&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ClassValue&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;twMerge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;clsx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et enfin un dernier fichier components.json à la racine du projet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://ui.shadcn.com/schema.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"style"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"new-york"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rsc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tsx"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tailwind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"css"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assets/styles/app.css"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"baseColor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"neutral"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"cssVariables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"aliases"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"components"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@/components"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"utils"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@/lib/utils"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ui"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@/components/ui"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"lib"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@/lib"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@/hooks"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"iconLibrary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lucide"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normalement c'est tout en terme de configuration.&lt;/p&gt;

&lt;p&gt;On va faire un test dans App.jsx pour voir si on peut ajouter un composant Alert de ShadCN.&lt;/p&gt;

&lt;p&gt;Installation du composant Alert:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx shadcn@latest add alert
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Puis, modifiez votre App.jsx tel que:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Alert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;AlertDescription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;AlertTitle&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/components/ui/alert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Terminal&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lucide-react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"relative isolate overflow-hidden bg-white px-6 py-24 sm:py-32 lg:overflow-visible lg:px-6"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mb-3"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Vite + React + Shadcn&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Alert&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"default | destructive"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Terminal&lt;/span&gt;&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AlertTitle&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Heads up!&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AlertTitle&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AlertDescription&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    You can add components and dependencies to your app using the cli.
                &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AlertDescription&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Alert&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et voilà !&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg2xhd4xpzuevy44rbwos.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg2xhd4xpzuevy44rbwos.png" alt="Capture ecran du resultat" width="800" height="213"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notez qu'à chaque modification de votre code, Vite recompile et met à jour la page dans votre navigateur.&lt;/p&gt;

&lt;p&gt;Amusez-vous bien !&lt;/p&gt;

</description>
      <category>symfony</category>
      <category>react</category>
      <category>shadcn</category>
      <category>vite</category>
    </item>
    <item>
      <title>DaisyUI 5 avec Symfony 6/7</title>
      <dc:creator>Guillaume</dc:creator>
      <pubDate>Tue, 13 May 2025 19:36:48 +0000</pubDate>
      <link>https://dev.to/gbtux/daisyui-5-avec-symfony-67-3io0</link>
      <guid>https://dev.to/gbtux/daisyui-5-avec-symfony-67-3io0</guid>
      <description>&lt;p&gt;Aujourd'hui, c'est mardi et c'est surtout ...&lt;a href="https://daisyui.com/" rel="noopener noreferrer"&gt;DaisyUI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Dans les dernières versions de Symfony (6 et 7) on a un nouveau composant pour gérer nos assets : AssetMapper.&lt;/p&gt;

&lt;p&gt;Le mini défi du jour : installer DaisyUI pour un nouveau projet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Création du projet Symfony
&lt;/h2&gt;

&lt;p&gt;Rien de bien compliqué à ce niveau, surtout avec la CLI Symfony (ici mon projet s'appelle FormerK):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;symfony new FormerK --webapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Installation du bundle Tailwind
&lt;/h2&gt;

&lt;p&gt;Encore une fois &lt;a href="https://symfony.com/bundles/TailwindBundle/current/index.html" rel="noopener noreferrer"&gt;la doc de Symfony&lt;/a&gt; est bien utile :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd FormerK
composer require symfonycasts/tailwind-bundle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configuration du bundle Tailwind
&lt;/h2&gt;

&lt;p&gt;Si vous faites tout de suite les commandes indiquées en fin d'installation, vous allez (avec la commande &lt;em&gt;bin/console tailwind:init&lt;/em&gt;) installer la version 3 de Tailwind.&lt;/p&gt;

&lt;p&gt;Ce n'est pas ce que l'on veut, puisque la dernière version de DaisyUI utilise Tailwind 4.&lt;/p&gt;

&lt;p&gt;Pour modifier la version de Tailwind utilisée, éditez le fichier config/packages/symfonycasts_tailwind.yaml et modifiez le comme suit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;symfonycasts_tailwind:
    # Specify the EXACT version of Tailwind CSS you want to use
    binary_version: 'v4.1.6'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lancez maintenant les commandes suivantes et observez le résultat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bin/console tailwind:init
bin/console tailwind:build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh7jm9iwwltfzmiv6yke0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh7jm9iwwltfzmiv6yke0.png" alt="Capture premier build de Tailwind"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation de DaisyUI
&lt;/h2&gt;

&lt;p&gt;Pour installer DaisyUI, là aussi, rien de compliqué : on va utiliser la commande de gestion des dépendances d'AssetMapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bin/console importmap:require daisyui@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;et voilà le résultat:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyf9yf5e7pexfhv5zhceo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyf9yf5e7pexfhv5zhceo.png" alt="Capture de l'import de DaisyUI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration de Tailwind pour utiliser DaisyUI
&lt;/h2&gt;

&lt;p&gt;Dans la version 4 de Tailwind, on utilise maintenant le "&lt;a class="mentioned-user" href="https://dev.to/plugin"&gt;@plugin&lt;/a&gt;". &lt;br&gt;
Nous n'avions pas configuré Tailwind dans notre projet, donc on va faire les 2 en même temps.&lt;br&gt;
Editez le fichier assets/styles/app.css comme suit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@import "tailwindcss";
@plugin "../vendor/daisyui/daisyui.index.js";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Puis relancez un build Tailwind avec la même commande :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bin/console tailwind:build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;et admirez le résultat:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuyr7bfvlulre7qj2aggm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuyr7bfvlulre7qj2aggm.png" alt="Résultat du build"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Un exemple !
&lt;/h2&gt;

&lt;p&gt;Créez un controleur dans votre application et dans votre template, ça doit donner ça avec l'exemple d'alert de DaisyUI :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{% extends 'base.html.twig' %}

{% block body %}
    &amp;lt;div role="alert" class="alert alert-success"&amp;gt;
        &amp;lt;svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24"&amp;gt;
            &amp;lt;path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /&amp;gt;
        &amp;lt;/svg&amp;gt;
        &amp;lt;span&amp;gt;Your purchase has been confirmed!&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
{% endblock %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Evidemment, il faut relancer la commande de compilation de tailwind:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bin/console tailwind:build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si vous ne voulez pas relancer la build à la main en permanence et que ça recompile à chaque changement sur vos templates, laissez tourner la commande suivante dans un terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bin/console tailwind:build --watch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et voilà le résultat :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr9dli7bbuef0y8q7j56m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr9dli7bbuef0y8q7j56m.png" alt="Affichage de la page"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enjoy !&lt;/p&gt;

</description>
      <category>symfony</category>
      <category>daisyui</category>
      <category>tailwindcss</category>
      <category>assetmapper</category>
    </item>
    <item>
      <title>React dans Symfony avec Vite</title>
      <dc:creator>Guillaume</dc:creator>
      <pubDate>Sat, 11 Feb 2023 21:19:36 +0000</pubDate>
      <link>https://dev.to/gbtux/react-dans-symfony-avec-vite-5e74</link>
      <guid>https://dev.to/gbtux/react-dans-symfony-avec-vite-5e74</guid>
      <description>&lt;p&gt;Problème du jour : je veux me mettre à React, mais avec un backend Symfony.&lt;/p&gt;

&lt;p&gt;Alors OK, vous allez me dire d'utiliser Webpack Encore et vous avez raison.... sauf que webpack est lent, surtout dans la phase de dev.&lt;/p&gt;

&lt;p&gt;Donc l'idée est de trouver comment intégrer Vite à notre projet et comment configurer Vite pour faire marcher React.&lt;/p&gt;

&lt;h2&gt;
  
  
  Créons notre projet Symfony (6)
&lt;/h2&gt;

&lt;p&gt;Rien de bien compliqué ici avec la CLI Symfony:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;symfony new --webapp reactSymfonyProject&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Intégrer Vite à mon projet Symfony
&lt;/h2&gt;

&lt;p&gt;Heureusement, quelqu'un s'est penché sur le problème pour nous (même si le faire à la main n'aurait pas non plus été infaisable). Il y a donc un bundle pour ça !&lt;/p&gt;

&lt;p&gt;Entrez dans le répertoire du projet et installez le bundle:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;composer require pentatrion/vite-bundle&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Voici le résultat de l'installation :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk89wjqyz7x79oljwkx12.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk89wjqyz7x79oljwkx12.png" alt="resultat installation du bundle" width="800" height="159"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On va donc faire comme indiqué, en sautant la première étape - en bref on laisse les paramètres par défaut.&lt;br&gt;
Si la deuxième vaut quand même le coup d'oeil (allez jeter un oeil au fichier vite.config.js comme indiqué), le fichier ne requiert aucune modification.  &lt;/p&gt;

&lt;p&gt;Suivez l'étape 3 et installez les dépendances Javascripts&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yarn install (ou npm install)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Enfin, on va aller modifier notre template Twig de base pour aller chercher le résultat de la compilation de Vite.&lt;/p&gt;

&lt;p&gt;Dans le fichier templates/base.html.twig, remplacez la ligne :&lt;br&gt;
&lt;code&gt;{{ encore_entry_link_tags('app') }}&lt;/code&gt;&lt;br&gt;
par&lt;br&gt;
&lt;code&gt;{{ vite_entry_link_tags('app') }}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;puis la ligne:&lt;br&gt;
&lt;code&gt;{{ encore_entry_script_tags('app') }}&lt;/code&gt;&lt;br&gt;
par &lt;br&gt;
&lt;code&gt;{{ vite_entry_script_tags('app', { dependency: 'react' }) }}&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
   Création d'un contrôleur de test
&lt;/h2&gt;

&lt;p&gt;A la racine du projet :&lt;/p&gt;

&lt;p&gt;&lt;code&gt;symfony console make:controller Test&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;On a maintenant une URL /test de disponible&lt;/p&gt;
&lt;h2&gt;
  
  
   Premier test
&lt;/h2&gt;

&lt;p&gt;On va démarrer tout ça et voir le résultat&lt;/p&gt;

&lt;p&gt;D'abord on va démarrer le serveur PHP interne:&lt;br&gt;
&lt;code&gt;symfony serve -d&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Puis on va compiler nos assets:&lt;br&gt;
&lt;code&gt;yarn dev&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Rendez-vous maintenant dans votre navigateur à l'adresse :&lt;br&gt;
&lt;a href="https://127.0.0.1:8000/test" rel="noopener noreferrer"&gt;https://127.0.0.1:8000/test&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note : le port 8000 peut différer sur votre installation. L'adresse est indiquée quand vous démarrez le serveur PHP.&lt;/p&gt;

&lt;p&gt;Maintenant constatez le résultat en ouvrant la console Javascript (F12):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv1f7ft8eibppo43q4ea0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv1f7ft8eibppo43q4ea0.png" alt="visualisation du résultat dans le navigateur" width="800" height="353"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On voit ici qu'on retrouve bien le résultat de notre script assets/app.js et que Vite est bien démarré en mode dev.&lt;/p&gt;

&lt;p&gt;Vous pouvez faire un test pour voir la puissance de Vite au regard de Webpack.&lt;/p&gt;

&lt;p&gt;Ajouter à la fin du fichier assets/app.js n'importe quel &lt;code&gt;console.log('plop')&lt;/code&gt;, et regardez le résultat apparaître dans la console instantanément, sans recharger la page du navigateur ! &lt;/p&gt;
&lt;h2&gt;
  
  
   OK pour Vite, mais pour React ?
&lt;/h2&gt;

&lt;p&gt;Pour l'instant on est d'accord, Vite marche mais pas de React en vue (notez la blague React - Vue).&lt;/p&gt;

&lt;p&gt;Il nous manque 2 choses : du code certes, mais aussi des dépendances.&lt;/p&gt;

&lt;p&gt;Installons-les:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn add &lt;span class="nt"&gt;-D&lt;/span&gt; @vitejs/plugin-react @types/react @types/react-dom
yarn add react react-dom

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On va maintenant transformer notre Javascript et faire un peu de React.&lt;/p&gt;

&lt;p&gt;Ouvrez le fichier assets/app.js et transformez-le comme suit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ReactDOM&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./App&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./app.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;ReactDOM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StrictMode&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StrictMode&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Comme vous le voyez, on va "rendre" notre application dans un element avec un ID "root".&lt;/p&gt;

&lt;p&gt;On ne l'a pas pour le moment. Pour cela, on va modifier notre page de test (fichier templates/test/index.html.twig) comme suit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{% extends 'base.html.twig' %}

{% block title %}Hello TestController!{% endblock %}

{% block body %}
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"root"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
{% endblock %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On va relancer Vite, et voir ce que ça donne....&lt;br&gt;
A la racine du projet, relancez Vite;&lt;br&gt;
&lt;code&gt;yarn dev&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Ouch, une grosse erreur apparaît et c'est bien normal :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpuuux0czlgw2ih4galqm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpuuux0czlgw2ih4galqm.png" alt="Erreur de compilation" width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On a mis du JSX dans un fichier avec l'extension ".js".&lt;br&gt;
Pour des raisons pratiques, on va renommer notre app.js en main.jsx&lt;/p&gt;

&lt;p&gt;N'oublions pas non plus de modifier notre vite.config.js en pointant aussi vers le fichier main.jsx au lieu de app.js. On va aussi en profiter pour dire à Vite qu'on utilise son plugin React. Votre fichier vite.config.js doit donc ressembler à ça:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;symfonyPlugin&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vite-plugin-symfony&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;react&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vitejs/plugin-react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;


&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nf"&gt;react&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nf"&gt;symfonyPlugin&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;rollupOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./assets/main.jsx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Relancez Vite, et constatez que vous avez une nouvelle erreur....normal, on a pas fini notre code ;)&lt;br&gt;
On dit dans notre fichier main.jsx de rendre le composant "App" mais on ne l'a pas créé ! C'est aussi pour des raisons de conflit qu'on a renommé notre app.js en main.jsx et non en app.jsx.&lt;/p&gt;

&lt;p&gt;Créez maintenant au même niveau que main.jsx un fichier App.jsx avec ce contenu :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;reactLogo&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./assets/react.svg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;viteLogo&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./images/vite.svg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"App"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://vitejs.dev"&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"_blank"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;viteLogo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"logo"&lt;/span&gt; &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Vite logo"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://reactjs.org"&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"_blank"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;reactLogo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"logo react"&lt;/span&gt; &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"React logo"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Vite + React&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          count is &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          Edit &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;code&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;assets/App.jsx&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;code&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; and save to test HMR
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"read-the-docs"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Click on the Vite and React logos to learn more
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: vous trouverez les logos de React et Vite en SVG sur Internet ;)&lt;/p&gt;

&lt;p&gt;Modifiez aussi le CSS de l'application (fichier assets/app.css) comme suit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nf"&gt;#root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1280px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.logo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;will-change&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="m"&gt;300ms&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.logo&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;drop-shadow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;2em&lt;/span&gt; &lt;span class="m"&gt;#646cff&lt;/span&gt;&lt;span class="n"&gt;aa&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.logo.react&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;drop-shadow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;2em&lt;/span&gt; &lt;span class="m"&gt;#61dafb&lt;/span&gt;&lt;span class="n"&gt;aa&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;logo-spin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0deg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;360deg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefers-reduced-motion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;no-preference&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;nth-of-type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logo&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;logo-spin&lt;/span&gt; &lt;span class="n"&gt;infinite&lt;/span&gt; &lt;span class="m"&gt;20s&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.read-the-docs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#888&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et voilà, normalement, vous devriez avoir cette page qui apparait dans votre navigateur :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr3f28554fe3lpcxbqkbh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr3f28554fe3lpcxbqkbh.png" alt="Capture résultat final" width="800" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>management</category>
      <category>productivity</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Créer un blog avec Strapi et Angular</title>
      <dc:creator>Guillaume</dc:creator>
      <pubDate>Sun, 08 May 2022 21:07:40 +0000</pubDate>
      <link>https://dev.to/gbtux/creer-un-blog-avec-strapi-et-angular-3fm3</link>
      <guid>https://dev.to/gbtux/creer-un-blog-avec-strapi-et-angular-3fm3</guid>
      <description>&lt;p&gt;Suite à la vidéo de Yoandev (&lt;a href="https://youtu.be/KGHJYoxlGRE" rel="noopener noreferrer"&gt;https://youtu.be/KGHJYoxlGRE&lt;/a&gt;), voici le petit (grand !) défit du jour : créer un blog avec Strapi et Angular.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quelques explications:
&lt;/h2&gt;

&lt;p&gt;Alors avant de commencer à coder, on va regarder pourquoi Strapi et pourquoi Angular.&lt;/p&gt;

&lt;p&gt;Strapi est ce qu'on appelle un "headless CMS".&lt;br&gt;
En bref, vous pouvez créer vos propres "type de contenu" (comme dans Drupal par exemple) et les exposer via une API. &lt;br&gt;
C'est vraiment pratique, et ça a beaucoup d'avantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;on s'évite la customisation de la partie "front" qui est souvent un enfer avec les CMS traditionnels&lt;/li&gt;
&lt;li&gt;on découple la partie backend (Strapi) et la partie "front" (ici Angular).&lt;/li&gt;
&lt;li&gt;on va disposer d'une API, donc potentiellement, vous pouvez aller la consommer avec une appli mobile ou un autre front.&lt;/li&gt;
&lt;li&gt;si jamais vous n'êtes pas satisfait d'Angular, rien ne vous empêche de la refactorer avec du VueJS, du React, ou ... du Symfony comme Yoan ;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Heu c'est quoi un "type de contenu"
&lt;/h2&gt;

&lt;p&gt;Hé ben c'est un objet, une "Entity" en Symfony, ou un Model dans une version plus générique.&lt;/p&gt;

&lt;p&gt;Dans notre exemple, notre type de contenu va être "Article".&lt;br&gt;
Il va contenir:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;un titre &lt;/li&gt;
&lt;li&gt;un contenu&lt;/li&gt;
&lt;li&gt;une image&lt;/li&gt;
&lt;li&gt;une date de publication &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vous avez tout compris ?&lt;br&gt;
Alors on y va pour le code !&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note : cet article est basé sur un article du blog de Strapi : &lt;a href="https://strapi.io/blog/build-a-blog-with-angular-js-strapi-and-apollo" rel="noopener noreferrer"&gt;Build a blog with Angular, Strapi and Apollo&lt;/a&gt; &lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Pré-requis
&lt;/h2&gt;

&lt;p&gt;Pas grand chose si ce n'est node et npm (et yarn ici)&lt;/p&gt;

&lt;p&gt;Personnellement, j'utilise NVM (&lt;a href="https://github.com/nvm-sh/nvm" rel="noopener noreferrer"&gt;https://github.com/nvm-sh/nvm&lt;/a&gt;) pour éviter les conflits de version de Node suivant les projets.&lt;br&gt;
Je vous donnerai les commandes à faire tout au long du tutoriel.&lt;/p&gt;
&lt;h1&gt;
  
  
  Backend
&lt;/h1&gt;
&lt;h2&gt;
  
  
  Installation de Strapi
&lt;/h2&gt;

&lt;p&gt;On va créer un répertoire pour la totalité du projet (backend + frontend) et rentrer à l'intérieur.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mkdir blog-strapi &amp;amp;&amp;amp; cd blog-strapi&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Puis on va créer notre application Strapi:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yarn create strapi-app backend --quickstart --no-run&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Le flag "--no-run" va éviter de la démarrer tout de suite après l'installation.&lt;/p&gt;

&lt;p&gt;Premier problème soulevé ici avec la version de node de ma distribution Linux.&lt;/p&gt;

&lt;p&gt;Ma solution: entrer dans le répertoire de l'application Strapi, supprimer le répertoire node_modules, puis créer un fichier .nvmrc pour "statifier" la version de Node utilisée pour le projet (ici 16+) et enfin réinstaller les dépendances (un simple yarn suffit).&lt;/p&gt;

&lt;p&gt;En bref:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd backend
rm -rf node_modules/
echo "16" &amp;gt; .nvmrc
yarn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Toujours dans le même répertoire "backend" (notre application Strapi), installons le plugin graphql avec&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yarn strapi install graphql&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Quand l'installation du plugin est finie, lançons notre application:&lt;br&gt;
&lt;code&gt;yarn strapi dev&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Normalement, la fenêtre de navigateur va s'ouvrir sur l'interface d'administration de Strapi, et vous demander de créer un compte admin.&lt;/p&gt;
&lt;h2&gt;
  
  
  Création de notre type de contenu "Article".
&lt;/h2&gt;

&lt;p&gt;Dans le menu de gauche, sélectionnez "Content-type Builder" puis le lien "Create new collection type".&lt;/p&gt;

&lt;p&gt;Donnez lui le nom "Article", tel que:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5i39ecbpx19obn2xuug8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5i39ecbpx19obn2xuug8.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Puis ajoutez (bouton "Add another field") les champs suivants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;title de type "Text" (required)&lt;/li&gt;
&lt;li&gt;content with type Rich Text (required)&lt;/li&gt;
&lt;li&gt;image with type Media (Single image) and (required)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note 1 : pour mettre un champ "required", allez dans l'autre onglet "advanced settings"&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkyccf6arb5xbulz1jo8s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkyccf6arb5xbulz1jo8s.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note 2 : le champ "published_at" est automatique, donc n'est pas à créer.&lt;/p&gt;

&lt;p&gt;On enregistre avec "Save" et puis on se lâche, on va créer nos premiers articles dans le backend, via le "Content Manager".&lt;/p&gt;

&lt;p&gt;Après la rédaction de quelques articles, on se dit que ça serait bien de tester si l'API nous les renvoie !&lt;/p&gt;

&lt;p&gt;Aucun problème, tout est prévu !&lt;/p&gt;

&lt;p&gt;Ah oui, sauf qu'avant, il faut régler la partie "permissions".&lt;br&gt;
L'API n'est pas accessible par défaut. Il faut, pour chaque type de contenu, déterminer qui a droit de quoi faire.&lt;/p&gt;

&lt;p&gt;Allez dans le menu de gauche "Settings", puis dans "USERS &amp;amp; PERMISSIONS PLUGIN" &amp;gt; Roles.&lt;/p&gt;

&lt;p&gt;Nous allons modifier le rôle "public" (avec le crayon) et ajouter les droits comme suit:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F76yrs3b7sv4e3c18a81h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F76yrs3b7sv4e3c18a81h.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Maintenant que les permissions sont réglées, on va pouvoir tester notre API.&lt;/p&gt;

&lt;p&gt;Rendez-vous sur l'interface de test de GraphQL avec votre navigateur &lt;a href="http://localhost:1337/graphql" rel="noopener noreferrer"&gt;ici&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Je vous laisse regarder ce qu'est graphQL, mais en bref, comparé à REST, cela va vous permettre de "choisir" les champs qui seront renvoyés par l'API. C'est comme REST mais en mieux, même si le langage de requêtage n'est pas forcément trivial.&lt;/p&gt;

&lt;p&gt;Note: l'interface de test GraphQL est très bien faite : elle dispose notamment d'une complétion automatique bien pratique !&lt;/p&gt;

&lt;p&gt;Sur l'interface, créez une requête telle que:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query Articles {
    articles {
    data {
      id, 
      attributes {
        title, 
        content, 
        image {
          data {
            attributes{
              url
            }
          }
        }
      }
    }
  } 
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Après execution, vous obtiendrez une réponse telle que :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "data": {
    "articles": {
      "data": [
        {
          "id": "1",
          "attributes": {
            "title": "Premier post",
            "content": "Ah enfin ce premier post !\nLe premier d'une longue série ;)\n\nEt puis je vais rappatrier ici tous les articles produits sur d'autres plateformes ;)\n\nA très bientôt !",
            "image": {
              "data": {
                "attributes": {
                  "url": "/uploads/wanderlabs_T_Ap9_Hue_Sl_KQ_unsplash_1_5e82873dce.jpg"
                }
              }
            }
          }
        },
        {
          "id": "2",
          "attributes": {
            "title": "Créer un blog avec Strapi",
            "content": "D'avoir, allez voir la vidéo de Yoandev ;)",
            "image": {
              "data": {
                "attributes": {
                  "url": "/uploads/photo_1499750310107_5fef28a66643_ixlib_rb_1_2_2f258ec988.1&amp;amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=870&amp;amp;q=80"
                }
              }
            }
          }
        }
      ]
    }
  }
} 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Catégories
&lt;/h2&gt;

&lt;p&gt;Chaque Article va disposer d'une "catégorie".&lt;/p&gt;

&lt;p&gt;Créez une nouvelle collection nommée "Categorie" avec un seul champ "name" de type "Text" et la sauvegarder.&lt;/p&gt;

&lt;p&gt;On va maintenant lier nos 2 types de contenu "Article" et "Categorie" : une catégorie est liée à 1 ou plusieurs articles.&lt;/p&gt;

&lt;p&gt;Ainsi, toujours dans le "Content-Type Builder", sélectionnez "Article" puis ajouter un champ de type "Relation" comme suit:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx4a75edxbaqooqoop3o6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx4a75edxbaqooqoop3o6.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;N'oubliez pas de cliquer sur "finish" dans la modale, puis sur "Save" pour enregistrer les modifications de notre type de contenu Article.&lt;/p&gt;

&lt;p&gt;Enfin, il faut régler la partie "droits" sur notre nouveau type "Category".&lt;br&gt;
Comme pour Article, on va dans le menu de gauche "Settings", puis dans "USERS &amp;amp; PERMISSIONS PLUGIN" &amp;gt; Roles &amp;gt; public, et modifier l'entrée Categorie comme suit :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frugnmefq7qcjb5w1c3wa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frugnmefq7qcjb5w1c3wa.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Maintenant que vous avez vos catégories, allez dans le Content-Manager, créez des catégories et associez chacun de vos articles à une catégorie.&lt;/p&gt;

&lt;p&gt;Vous pouvez de nouveau tester votre API avec le client GraphQL avec une requête telle que:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query Articles {
    articles {
    data {
      id, 
      attributes {
        title, 
        categorie {
          data {
            id, 
            attributes {
              name
            }
          }
        },
        content, 
        image {
          data {
            attributes{
              url
            }
          }
        }
      }
    }
  } 
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vous obtiendrez alors un résultat de ce type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "data": {
    "articles": {
      "data": [
        {
          "id": "1",
          "attributes": {
            "title": "Premier post",
            "categorie": {
              "data": {
                "id": "3",
                "attributes": {
                  "name": "blog"
                }
              }
            },
            "content": "Ah enfin ce premier post !\nLe premier d'une longue série ;)\n\nEt puis je vais rappatrier ici tous les articles produits sur d'autres plateformes ;)\n\nA très bientôt !",
            "image": {
              "data": {
                "attributes": {
                  "url": "/uploads/wanderlabs_T_Ap9_Hue_Sl_KQ_unsplash_1_5e82873dce.jpg"
                }
              }
            }
          }
        },
        {
          "id": "2",
          "attributes": {
            "title": "Créer un blog avec Strapi",
            "categorie": {
              "data": {
                "id": "2",
                "attributes": {
                  "name": "strapi"
                }
              }
            },
            "content": "D'avoir, allez voir la vidéo de Yoandev ;)",
            "image": {
              "data": {
                "attributes": {
                  "url": "/uploads/photo_1499750310107_5fef28a66643_ixlib_rb_1_2_2f258ec988.1&amp;amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=870&amp;amp;q=80"
                }
              }
            }
          }
        }
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Résumé du backend
&lt;/h3&gt;

&lt;p&gt;Maintenant, on a un beau backend avec Strapi qui nous fournit une belle API GraphQL pour délivrer des articles avec des catégories.&lt;/p&gt;

&lt;h1&gt;
  
  
   Frontend
&lt;/h1&gt;

&lt;p&gt;Dans notre console, on va laisser Strapi démarré, et on va revenir dans le répertoire de base du projet&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cd blog-strapi&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
   Création de l'application Angular
&lt;/h2&gt;

&lt;p&gt;D'abord, on va installer le CLI Angular :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo npm install -g @angular/cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Puis créons notre application frontend (acceptez l'ajout de "Angular Routing":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ng new frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;L'application est maintenant créée, on va maintenant la lancer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd frontend
ng serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ouvrez votre navigateur à l'adresse &lt;a href="http://localhost:4200/" rel="noopener noreferrer"&gt;http://localhost:4200/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F83i28bi2s45wglufkdzu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F83i28bi2s45wglufkdzu.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Installons maintenant le plugin Apollo / client GraphQL à notre application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ng add apollo-angular
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1pttw6xs709m6nq914p6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1pttw6xs709m6nq914p6.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On va aussi ajouter la bibliothèque de composants CSS "UIKit" :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add uikit jquery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ajoutons maintenant les dépendances javascript du kit à notre code.&lt;br&gt;
Pour cela, ouvrez le fichier &lt;code&gt;angular.json&lt;/code&gt; et trouvez la clé&lt;br&gt;
projects &amp;gt; frontend &amp;gt; architect &amp;gt; build &amp;gt; options &amp;gt; scripts qui doit être un tableau vide et configurez-le tel que:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"scripts": [
    "node_modules/jquery/dist/jquery.min.js",
    "node_modules/uikit/dist/js/uikit.min.js",
    "node_modules/uikit/dist/js/uikit-icons.min.js"
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On va aussi ajouter la partie CSS à notre projet:&lt;br&gt;
Modifiez le fichier &lt;code&gt;src/style.css&lt;/code&gt; qui doit être vide comme suit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/* You can add global styles to this file, and also import other style files */
@import "../node_modules/uikit/dist/css/uikit.min.css";
@import "../node_modules/uikit/dist/css/uikit.css";
@import "../node_modules/uikit/dist/css/uikit-core.css";
@import url("https://fonts.googleapis.com/css?family=Staatliches");

a {
  text-decoration: none;
}

h1 {
  font-family: Staatliches;
  font-size: 120px;
}

#category {
  font-family: Staatliches;
  font-weight: 500;
}

#title {
  letter-spacing: 0.4px;
  font-size: 22px;
  font-size: 1.375rem;
  line-height: 1.13636;
}

#banner {
  margin: 20px;
  height: 800px;
}

#editor {
  font-size: 16px;
  font-size: 1rem;
  line-height: 1.75;
}

.uk-navbar-container {
  background: #fff !important;
  font-family: Staatliches;
}

img:hover {
  opacity: 1;
  transition: opacity 0.25s cubic-bezier(0.39, 0.575, 0.565, 1);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Nav component
&lt;/h3&gt;

&lt;p&gt;On va créer notre premier composant, la barre de navigation :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ng generate c nav --skip-import
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4 fichiers sont ainsi créés, mais on va uniquement travailler avec les fichiers .html and .ts . &lt;br&gt;
Les fichiers .html sont vos templates (la partie visible) et les .ts sont le "moteur" de votre composant, ce qu'il fait, comment il se comporte, etc.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjea6cmshjnkmgccro07k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjea6cmshjnkmgccro07k.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Maintenant, on va modifier notre composant "nav" pour aller chercher les catégories et les afficher en haut à droite de notre barre de navigation.&lt;/p&gt;

&lt;p&gt;Ouvrez le fichier &lt;code&gt;nav/nav.component.ts&lt;/code&gt; et modifiez votre code comme suit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Component, OnInit } from '@angular/core';
import { Apollo } from "apollo-angular";
import gql from "graphql-tag";

@Component({
  selector: 'app-nav',
  templateUrl: './nav.component.html',
  styleUrls: ['./nav.component.css']
})

export class NavComponent implements OnInit {

  data: any = {};
  loading = true;
  errors: any;

  constructor(private apollo: Apollo) {}

  ngOnInit(): void {
    this.apollo.watchQuery({
        query: gql`
          query Categories {
            categories {
              data {
                id, 
                attributes {
                  name
                }
              }
            }
          }
        `
      })
      .valueChanges.subscribe((result: any) =&amp;gt; {
        this.data = result?.data?.categories;
        this.loading = result.loading;
        this.errors = result.error;
      });
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ici, on va chercher nos catégories avec Apollo lors de l'initialisation du composant avec une requête graphql.&lt;/p&gt;

&lt;p&gt;La variable &lt;code&gt;data&lt;/code&gt; va contenir nos catégories.&lt;/p&gt;

&lt;p&gt;Maintenant, modifions le template pour les afficher !&lt;/p&gt;

&lt;p&gt;Ouvrez &lt;code&gt;nav/nav.component.html&lt;/code&gt; et modifiez le code comme suit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;nav class="uk-navbar-container" uk-navbar&amp;gt;
    &amp;lt;div class="uk-navbar-left"&amp;gt;
        &amp;lt;ul class="uk-navbar-nav"&amp;gt;
        &amp;lt;li class="uk-active"&amp;gt;&amp;lt;a href="#"&amp;gt;Strapi blog&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
        &amp;lt;/ul&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div class="uk-navbar-right"&amp;gt;
        &amp;lt;ul *ngIf="data" class="uk-navbar-nav"&amp;gt;
            &amp;lt;li *ngFor="let category of data.data" class="uk-active"&amp;gt;
                &amp;lt;a routerLink="/category/{{ category.id }}" routerLinkActive="active" class="uk-link-reset"&amp;gt;
                {{ category.attributes.name }}
                &amp;lt;/a&amp;gt;
            &amp;lt;/li&amp;gt;
        &amp;lt;/ul&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/nav&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dans cette vue, on peut bien accéder à notre variable "data" (qui contient les catégories) et sa variable "data" qui contient le tableau des catégories.&lt;br&gt;
On fait une boucle &lt;code&gt;ngFor&lt;/code&gt; dessus pour afficher une à une nos catégories et créer un lien avec.&lt;/p&gt;

&lt;p&gt;Note : "data.data" n'est pas très lisible, mais cela est dû à la forme du JSON qui est renvoyé de l'API de Strapi.&lt;br&gt;
Voici un exemple de ce que vous renvoie l'API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"data":{"categories":{"data":[{"id":"2","attributes":{"name":"strapi","__typename":"Categorie"},"__typename":"CategorieEntity"},{"id":"3","attributes":{"name":"blog","__typename":"Categorie"},"__typename":"CategorieEntity"}],"__typename":"CategorieEntityResponseCollection"}}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La variable "data" dans notre .ts contient bien "result" (l'ensemble du résultat), ".data" pour accéder au premier élément "data" du résultat (qui contient alors "categories":{"data":[...]}....&lt;br&gt;
Dans la vue, on est donc obligé de prendre "data.data" pour avoir le tableau. &lt;/p&gt;

&lt;p&gt;Encore 2 petites modifications et on est bon.&lt;/p&gt;

&lt;p&gt;La première, c'est de déclarer notre module "NavComponent" dans la liste des modules.&lt;/p&gt;

&lt;p&gt;Pour cela, modifiez &lt;code&gt;app.modules.ts&lt;/code&gt; comme suit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
import { NavComponent } from "./nav/nav.component";
...

declarations: [
  AppComponent,
  NavComponent
],
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On importe simplement le module, et on l'ajoute dans le tableau "declarations".&lt;/p&gt;

&lt;p&gt;Enfin, on va modifier notre template de base de l'application pour ajouter notre barre de navigation.&lt;/p&gt;

&lt;p&gt;Pour cela, ouvrez le fichier &lt;code&gt;app.component.html&lt;/code&gt;, supprimez tout son contenu, et ajoutez simplement le code suivant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;app-nav&amp;gt;&amp;lt;/app-nav&amp;gt;
&amp;lt;router-outlet&amp;gt;&amp;lt;/router-outlet&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ici, on ajoute notre composant "nav" (la balise  " et on laisse le router (balise router-outlet) gérer le reste de la page.&lt;/p&gt;

&lt;p&gt;Contemplez le résultat dans votre navigateur :&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmsuvs4sfe7ow3crarwsl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmsuvs4sfe7ow3crarwsl.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Mes 2 catégories sont bien &lt;code&gt;Strapi&lt;/code&gt; et &lt;code&gt;Blog&lt;/code&gt;. Victoire !&lt;/p&gt;
&lt;h3&gt;
  
  
   Articles component
&lt;/h3&gt;

&lt;p&gt;On va appliquer le même raisonnement à notre 2ème composant, "ArticlesComponent", qui va lister tous les articles.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Création du composant&lt;/li&gt;
&lt;li&gt;Modification de la logique du composant dans le .ts&lt;/li&gt;
&lt;li&gt;Modification de la vue du composant&lt;/li&gt;
&lt;li&gt;Référencement du composant dans la liste des modules&lt;/li&gt;
&lt;li&gt;Modification du routage de l'application&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Création du composant
&lt;/h4&gt;

&lt;p&gt;A la racine du projet, en ligne de commande, créons notre composant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ng generate c articles/articles --skip-import 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notez ici que j'ai ajouté "articles/" devant le nom du composant. ça a Pour effet de créer un répertoire englobant pour tous nos futurs composants qui traiteront d'articles (comme le prochain qui affichera un article). Je trouve que c'est une bonne pratique, surtout dans les grosses applications, sinon on s'y perd vite....&lt;/p&gt;

&lt;p&gt;Comme pour la barre de navigation, modifiez le .ts du composant &lt;code&gt;src/app/articles/articles/articles.components.ts&lt;/code&gt; comme suit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Component, OnInit } from '@angular/core';
import { Apollo } from "apollo-angular";
import gql from "graphql-tag";

@Component({
  selector: 'app-articles',
  templateUrl: './articles.component.html',
  styleUrls: ['./articles.component.css']
})
export class ArticlesComponent implements OnInit {

  data: any = {};
  loading = true;
  errors: any;
  leftArticlesCount: any;
  leftArticles?: any[];
  rightArticles?: any[];

  constructor(private apollo: Apollo) { }

  ngOnInit(): void {
    this.apollo.watchQuery({
      query: gql`
        query Articles {
          articles {
            data {
              id, 
              attributes {
                title, 
                categorie {
                  data {
                    id, 
                    attributes {
                      name
                    }
                  }
                },
                content, 
                image {
                  data {
                    attributes{
                      url
                    }
                  }
                }
              }
            }
          }
        }` 
      })
      .valueChanges.subscribe((result: any) =&amp;gt; {
        this.data = result?.data?.articles;
        this.leftArticlesCount = Math.ceil(this.data?.data.length / 5);
        this.leftArticles = this.data?.data.slice(0, this.leftArticlesCount);

        this.rightArticles = this.data?.data.slice(
          this.leftArticlesCount,
          this.data?.data.length
        );
        this.loading = result.loading;
        this.errors = result.error;
      });
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ici, pas grand chose de nouveau puisqu'on a le même principe que la barre de navigation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;une requête graphql&lt;/li&gt;
&lt;li&gt;un traitement des résultats avec la consitution de 2 tableaux : 1 pour la partie gauche (les grandes images) et 1 pour la partie droite&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Modifions maintenant le template associé &lt;code&gt;src/app/articles/articles/articles.component.html&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div class="uk-section"&amp;gt;
    &amp;lt;div class="uk-container uk-container-large"&amp;gt;
      &amp;lt;h1&amp;gt;Strapi blog&amp;lt;/h1&amp;gt;

      &amp;lt;div class="uk-child-width-1-2" uk-grid&amp;gt;
        &amp;lt;div&amp;gt;
          &amp;lt;a
            routerLink="/article/{{ article.id }}"
            routerLinkActive="active"
            *ngFor="let article of leftArticles"
            class="uk-link-reset"
          &amp;gt;
            &amp;lt;div class="uk-card uk-card-muted"&amp;gt;
              &amp;lt;div *ngIf="article.attributes.image" class="uk-card-media-top"&amp;gt;
                &amp;lt;img
                  src="http://localhost:1337{{ article.attributes.image.data.attributes.url }}"
                  alt=""
                  height="100"
                /&amp;gt;
              &amp;lt;/div&amp;gt;
              &amp;lt;div class="uk-card-body"&amp;gt;
                &amp;lt;p
                  id="category"
                  *ngIf="article.attributes.categorie"
                  class="uk-text-uppercase"
                &amp;gt;
                  {{ article.attributes.categorie.data.attributes.name }}
                &amp;lt;/p&amp;gt;
                &amp;lt;p id="title" class="uk-text-large"&amp;gt;{{ article.attributes.title }}&amp;lt;/p&amp;gt;
              &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
          &amp;lt;/a&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
          &amp;lt;div class="uk-child-width-1-2@m uk-grid-match" uk-grid&amp;gt;
            &amp;lt;a
              routerLink="/article/{{ article.id }}"
              routerLinkActive="active"
              *ngFor="let article of rightArticles"
              class="uk-link-reset"
            &amp;gt;
              &amp;lt;div class="uk-card uk-card-muted"&amp;gt;
                &amp;lt;div *ngIf="article.attributes.image" class="uk-card-media-top"&amp;gt;
                  &amp;lt;img
                    src="http://localhost:1337{{ article.attributes.image.data.attributes.url }}"
                    alt=""
                    height="100"
                  /&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div class="uk-card-body"&amp;gt;
                  &amp;lt;p id="category" *ngIf="article.attributes.categorie" class="uk-text-uppercase"&amp;gt;
                    {{ article.attributes.categorie.data.attributes.name }}
                  &amp;lt;/p&amp;gt;
                  &amp;lt;p id="title" class="uk-text-large"&amp;gt;{{ article.attributes.title }}&amp;lt;/p&amp;gt;
                &amp;lt;/div&amp;gt;
              &amp;lt;/div&amp;gt;
            &amp;lt;/a&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Afin de contruire et retrouver les chemins, vous pouvez vous référer au JSON produit par l'API tel que:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"data":{"articles":{"data":[{"id":"1","attributes":{"title":"Premier post","categorie":{"data":{"id":"3","attributes":{"name":"blog","__typename":"Categorie"},"__typename":"CategorieEntity"},"__typename":"CategorieEntityResponse"},"content":"Ah enfin ce premier post !\nLe premier d'une longue série ;)\n\nEt puis je vais rappatrier ici tous les articles produits sur d'autres plateformes ;)\n\nA très bientôt !","image":{"data":{"attributes":{"url":"/uploads/wanderlabs_T_Ap9_Hue_Sl_KQ_unsplash_1_5e82873dce.jpg","__typename":"UploadFile"},"__typename":"UploadFileEntity"},"__typename":"UploadFileEntityResponse"},"__typename":"Article"},"__typename":"ArticleEntity"},{"id":"2","attributes":{"title":"Créer un blog avec Strapi","categorie":{"data":{"id":"2","attributes":{"name":"strapi","__typename":"Categorie"},"__typename":"CategorieEntity"},"__typename":"CategorieEntityResponse"},"content":"D'avoir, allez voir la vidéo de Yoandev ;)","image":{"data":{"attributes":{"url":"/uploads/photo_1499750310107_5fef28a66643_ixlib_rb_1_2_2f258ec988.1&amp;amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=870&amp;amp;q=80","__typename":"UploadFile"},"__typename":"UploadFileEntity"},"__typename":"UploadFileEntityResponse"},"__typename":"Article"},"__typename":"ArticleEntity"}],"__typename":"ArticleEntityResponseCollection"}}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Référençons maintenant notre composant dans les modules.&lt;br&gt;
Pour cela, modifiez &lt;code&gt;app.modules.ts&lt;/code&gt; comme suit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
import { ArticlesComponent } from "./articles/articles/articles.component"
...

declarations: [
  AppComponent,
  NavComponent,
  ArticlesComponent
],
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modifions maintenant le "Router" de notre application, dans le fichier &lt;code&gt;app-routing.module.ts&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
import { ArticlesComponent } from "./articles/articles/articles.component"

const routes: Routes = [
  { path: "", component: ArticlesComponent }
];
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On importe notre composant, et on déclare une route.&lt;br&gt;
Ici, on dit que notre ArticlesComponent sera la page d'accueil puisque le "path" est vide.&lt;/p&gt;

&lt;p&gt;Constatez le résultat dans votre navigateur:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdttbp55xqmq4vjml25kx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdttbp55xqmq4vjml25kx.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Article component
&lt;/h3&gt;

&lt;p&gt;On a listé nos articles, mais maintenant, que se passe-t-il si je clique que un article ? rien !&lt;/p&gt;

&lt;p&gt;Réglons ce problème en créant un "ArticleComponent" pour afficher un article :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ng generate c articles/article --skip-import
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Là encore, j'ajoute "articles/" pour mettre notre composant dans le répertoire des composants "articles".&lt;/p&gt;

&lt;p&gt;Encore une fois, commençons par modifier notre partie "comportement" du composant avec son fichier &lt;code&gt;src/app/articles/article/article.component.ts&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Component, OnInit } from '@angular/core';
import { Apollo } from "apollo-angular";
import gql from "graphql-tag";
import { ActivatedRoute } from "@angular/router";

@Component({
  selector: 'app-article',
  templateUrl: './article.component.html',
  styleUrls: ['./article.component.css']
})
export class ArticleComponent implements OnInit {

  data: any = {};
  image: any;
  title: any;
  content: any;
  loading = true;
  errors: any;

  constructor(private apollo: Apollo, private route: ActivatedRoute) { }

  ngOnInit(): void {
    this.apollo.watchQuery({
      query: gql`
        query Articles($id: ID!) {
          article(id: $id) {
            data {
              id,
              attributes {
                title, 
                content, 
                categorie {
                  data {
                    id, 
                    attributes {
                      name
                    }
                  }
                },
                image {
                  data {
                    attributes{
                      url
                    }
                  }
                }
              }
            }
          }
        }` 
      ,
      variables: {
        id: this.route.snapshot.paramMap.get("id")
      }
    })
    .valueChanges.subscribe(result =&amp;gt; {
      this.data = result.data;
      this.image = this.data?.article.data.attributes.image?.data?.attributes?.url
      this.title = this.data?.article.data.attributes.title
      this.content = this.data?.article.data.attributes.content
      this.loading = result.loading;
      this.errors = result.errors;
    });
  }

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Puis, modifions notre template &lt;code&gt;src/app/articles/article/article.component.html&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div id="banner"
  class="uk-height-small uk-flex uk-flex-center uk-flex-middle uk-background-cover uk-light uk-padding"
  [style.background-image]="
    'url(http://localhost:1337' + image + ')'
  "
  uk-img
&amp;gt;
  &amp;lt;h1&amp;gt;{{ title }}&amp;lt;/h1&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;div class="uk-section"&amp;gt;
  &amp;lt;div class="uk-container uk-container-small"&amp;gt;
    &amp;lt;p&amp;gt;
        {{ content }}
    &amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modifiez aussi &lt;code&gt;app.module.ts&lt;/code&gt; pour ajouter le module ArticleComponent.&lt;br&gt;
Puis ajoutez la route suivante dans le fichier app-routing.module.ts&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{ path: "articles/:id", component: ArticleComponent }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vous pouvez maintenant cliquer dans la page d'accueil sur un article pour afficher notre nouvelle page.&lt;/p&gt;

&lt;p&gt;Si ça marche bien, il y a un petit soucis dans l'affichage.&lt;br&gt;
En effet, le code html saisi dans Strapi est rendu tel quel.&lt;br&gt;
Dans le backoffice de Strapi, vous saisissez votre contenu riche (le champs "content") en markdown.&lt;/p&gt;

&lt;p&gt;L'affichage est donc rendu avec les marqueurs Markdown.&lt;br&gt;
Voici un exemple:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyeqk8nlruyq6xylf5201.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyeqk8nlruyq6xylf5201.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ici, on voit bien que la phrase "essai de texte riche", que j'ai mis en gras dans Strapi, donc encadré avec 2 étoiles de chaque côté, n'est pas rendu en gras mais avec les marqueurs.&lt;/p&gt;

&lt;p&gt;Pour "convertir" le code Markdown en code HTML, on va utiliser une librairie Javascript : ngx-markdown.&lt;/p&gt;

&lt;p&gt;Installons et configurons cette librairie:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add ngx-markdown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ajoutez maintenant le module dans &lt;code&gt;app.modules.ts&lt;/code&gt;, dans les imports tel que:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
import { MarkdownModule } from "ngx-markdown";
...

imports: [
  MarkdownModule.forRoot(),
  RouterModule.forRoot(appRoutes, { enableTracing: true }),
  BrowserModule,
  AppRoutingModule,
  GraphQLModule,
  HttpClientModule
],
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On va utiliser le plugin pour transformer notre variable "content" du markdown vers du HTML.&lt;br&gt;
Dans &lt;code&gt;article.component.ts&lt;/code&gt; ajoutez un import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
import { MarkdownService } from 'ngx-markdown';
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;puis modifier dans ngOnInit l'affectation de la variable content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;this.content = this.markdownService.compile(this.data?.article.data.attributes.content)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Maintenant c'est bon !&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdagx6fuizc7ugmy2vk7v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdagx6fuizc7ugmy2vk7v.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Categorie component
&lt;/h3&gt;

&lt;p&gt;Il nous reste un dernier composant à construire, celui pour afficher les articles d'une catégorie, celui qui affichera les articles liés à une catégorie (les liens du haut de la barre de navigation).&lt;/p&gt;

&lt;p&gt;Créons le :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ng generate c category --skip-import
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On fait comme d'habitude, on modifier le fichier de classe &lt;code&gt;src/app/category/category.component.ts&lt;/code&gt; tel que:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Component, OnInit } from '@angular/core';
import { Apollo } from "apollo-angular";
import gql from "graphql-tag";
import { ActivatedRoute, ParamMap } from "@angular/router";

@Component({
  selector: 'app-category',
  templateUrl: './category.component.html',
  styleUrls: ['./category.component.css']
})
export class CategoryComponent implements OnInit {

  data: any = {};
  category: any = {};
  loading = true;
  errors: any;
  leftArticlesCount: any;
  leftArticles?: any[];
  rightArticles?: any[];
  id: any;
  queryCategorie: any;

  constructor(private apollo: Apollo, private route: ActivatedRoute) { }

  ngOnInit(): void {
    this.route.paramMap.subscribe((params: ParamMap) =&amp;gt; {
      this.id = params.get("id");
      this.queryCategorie = this.apollo.watchQuery({
        query: gql`
          query Categorie($id: ID!) {
            categorie(id: $id) {
              data {
                id,
                attributes {
                  name,
                  articles {
                    data {
                      id,
                      attributes {
                        title,
                        content,
                        createdAt,
                        image {
                          data {
                            attributes{
                              url
                            }
                          }
                        }

                      }
                    }
                  }
                }
              }
            }
          }
        ` 
        ,
        variables: {
          id: this.id
        }
      })
      .valueChanges.subscribe(result =&amp;gt; {
        this.data = result.data;

        this.category = this.data.categorie.data.attributes.name
        this.leftArticlesCount = Math.ceil(this.data?.categorie.data.attributes.articles.data.length / 5);

        this.leftArticles = this.data?.categorie.data.attributes.articles.data.slice(0, this.leftArticlesCount);
        this.rightArticles = this.data?.categorie.data.attributes.articles.data.slice(
          this.leftArticlesCount,
          this.data?.categorie.data.attributes.articles.data.length
        );

        this.loading = result.loading;
        this.errors = result.errors;
      });
    });  
  }

  ngOnDestroy() {
    this.queryCategorie.unsubscribe();
  }

}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rien d'exceptionnel ici :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;on crée une requête graphql&lt;/li&gt;
&lt;li&gt;on lance la requête à la création du composant, et on construit les 2 listes (leftArticles, rightArticles) &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Puis, modifions la partie HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div class="uk-section"&amp;gt;
    &amp;lt;div class="uk-container uk-container-large"&amp;gt;
      &amp;lt;h1&amp;gt;{{ category }}&amp;lt;/h1&amp;gt;

      &amp;lt;div class="uk-child-width-1-2" uk-grid&amp;gt;
        &amp;lt;div&amp;gt;
          &amp;lt;a
            routerLink="/articles/{{ article.id }}"
            routerLinkActive="active"
            *ngFor="let article of leftArticles"
            class="uk-link-reset"
          &amp;gt;
            &amp;lt;div class="uk-card uk-card-muted"&amp;gt;
              &amp;lt;div *ngIf="article.attributes.image" class="uk-card-media-top"&amp;gt;
                &amp;lt;img
                  src="http://localhost:1337{{ article.attributes.image.data.attributes.url }}"
                  alt=""
                  height="100"
                /&amp;gt;
              &amp;lt;/div&amp;gt;
              &amp;lt;div class="uk-card-body"&amp;gt;
                &amp;lt;p
                  id="category"
                  class="uk-text-uppercase"
                &amp;gt;
                  {{ category }}
                &amp;lt;/p&amp;gt;
                &amp;lt;p id="title" class="uk-text-large"&amp;gt;{{ article.attributes.title }}&amp;lt;/p&amp;gt;
              &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
          &amp;lt;/a&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
          &amp;lt;div class="uk-child-width-1-2@m uk-grid-match" uk-grid&amp;gt;
            &amp;lt;a
              routerLink="/articles/{{ article.id }}"
              routerLinkActive="active"
              *ngFor="let article of rightArticles"
              class="uk-link-reset"
            &amp;gt;
              &amp;lt;div class="uk-card uk-card-muted"&amp;gt;
                &amp;lt;div *ngIf="article.attributes.image" class="uk-card-media-top"&amp;gt;
                  &amp;lt;img
                    src="http://localhost:1337{{ article.attributes.image.data.attributes.url }}"
                    alt=""
                    height="100"
                  /&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div class="uk-card-body"&amp;gt;
                  &amp;lt;p
                    id="category"
                    *ngIf="article.category"
                    class="uk-text-uppercase"
                  &amp;gt;
                    {{ article.category.name }}
                  &amp;lt;/p&amp;gt;
                  &amp;lt;p id="title" class="uk-text-large"&amp;gt;{{ article.title }}&amp;lt;/p&amp;gt;
                &amp;lt;/div&amp;gt;
              &amp;lt;/div&amp;gt;
            &amp;lt;/a&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Référençons maintenant notre composant dans les modules.&lt;br&gt;
Pour cela, modifiez &lt;code&gt;app.modules.ts&lt;/code&gt; comme suit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
import { ArticlesComponent } from "./articles/articles/articles.component"
...

declarations: [
  AppComponent,
  NavComponent,
  ArticlesComponent,
  ArticleComponent,
  CategoryComponent
],
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enfin, ajoutez la route suivante dans le fichier app-routing.module.ts (avec son import)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
import { ArticleComponent } from "./articles/article/article.component"
...
{ path: "category/:id", component: CategoryComponent }
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Loin d'être un spécialiste d'Angular, et de GraphQL, on arrive néanmoins à créer un client facilement pour notre API Strapi.&lt;/p&gt;

&lt;p&gt;La prise en main de Strapi est en revanche vraiment facile !&lt;br&gt;
Réellement impressionné par la dernière version, vite installé.&lt;/p&gt;

&lt;p&gt;Merci à Yoandev pour l'inspiration de cet article.&lt;br&gt;
Retrouvez sa chaine et sa vidéo ici : (&lt;a href="https://youtu.be/KGHJYoxlGRE" rel="noopener noreferrer"&gt;https://youtu.be/KGHJYoxlGRE&lt;/a&gt;)&lt;/p&gt;

</description>
      <category>strapi</category>
      <category>angular</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>CMS en Symfony : le routing</title>
      <dc:creator>Guillaume</dc:creator>
      <pubDate>Mon, 28 Feb 2022 23:22:59 +0000</pubDate>
      <link>https://dev.to/gbtux/cms-en-symfony-le-routing-2ngo</link>
      <guid>https://dev.to/gbtux/cms-en-symfony-le-routing-2ngo</guid>
      <description>&lt;p&gt;Maintenant qu'on a regardé un peu la partie fonctionnelle, on va faire un peu de technique.&lt;/p&gt;

&lt;p&gt;L'une des fonctionnalités à laquelle chaque utilisateur de CMS s'attend à trouver, est la gestion du "routing" : "je crée une page "Actualités" et je m'attends à pouvoir l'afficher si je tape "&lt;a href="http://www.monsupersite.com/actualites"&gt;www.monsupersite.com/actualites&lt;/a&gt;".&lt;/p&gt;

&lt;p&gt;2 choses à gérer :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pouvoir créer des pages&lt;/li&gt;
&lt;li&gt;créer un "slug" pour chaque page&lt;/li&gt;
&lt;li&gt;faire en sorte que chaque page soit automatiquement enregistrée dans les routes du CMS pour les rendre disponibles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On va donc commencer par ... la fin (je laisse la structure des pages pour le prochain épisode !).&lt;/p&gt;

&lt;p&gt;Evidemment, notre framework préféré nous donne la possibilité de faire ça en ... 5 minutes chrono !&lt;/p&gt;

&lt;h1&gt;
  
  
  Routing as a Service.
&lt;/h1&gt;

&lt;p&gt;Quand on utilise Symfony, on a l'habitude d'utiliser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;soit le fichier yaml de routing (basique, mais ça sert parfois, valable aussi en XML ou en PHP)&lt;/li&gt;
&lt;li&gt;soit les annotations dans les controller (ou les attributs maintenant, hein les dinosaures ! )&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Evidemment, que ce soit l'un ou l'autre, ça ne répond pas à notre besoin : on a besoin de quelque chose de dynamique qui va cherche la liste de nos pages en base de données et qui les "charge".&lt;/p&gt;

&lt;p&gt;Hé bien c'est exactement ce que Symfony nous propose !&lt;/p&gt;

&lt;p&gt;On appelle ça un "route loader". Pour les curieux (et parce que je vous invite à lire la doc !) c'est &lt;a href="https://symfony.com/doc/current/routing/custom_route_loader.html"&gt;ici&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Nous, on a une contrainte supplémentaire : certes, on crée un CMS, mais on veut aussi pouvoir faire du Symfony "natif". En bref, si je veux créer une route "hors CMS", je dois pouvoir le faire.&lt;/p&gt;

&lt;p&gt;Qu'à cela ne tienne, on a aussi cette possibilité.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le fichier de définition des routes
&lt;/h2&gt;

&lt;p&gt;Dans Symfony, le fichier de base pour définir les routes est le fichier "config/routes.yaml".&lt;/p&gt;

&lt;p&gt;Par défaut, dans une application Symfony (version 6) il doit ressembler à quelque chose du genre :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;controllers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../src/Controller/&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;annotation&lt;/span&gt;

&lt;span class="na"&gt;kernel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../src/Kernel.php&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;annotation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On va ajouter quelques lignes en haut du fichier tel que:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cms_routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cms&lt;/span&gt;

&lt;span class="na"&gt;controllers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../src/Controller/&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;annotation&lt;/span&gt;

&lt;span class="na"&gt;kernel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../src/Kernel.php&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;annotation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On ajoute un nouveau "groupe de routes" tel que:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cms_routes : nom arbitraire. Mettez "toto24" si ça vous fait plaisir ;)&lt;/li&gt;
&lt;li&gt;resource: . : en fait on ignore ce paramètre, d'où le point&lt;/li&gt;
&lt;li&gt;type: cms : heu c'est pas natif ce type ? Non, c'est pas natif ! C'est là où on intervient : on va aller créer notre loader, responsable de ce type de route.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's go.&lt;/p&gt;

&lt;h2&gt;
  
  
  A votre service :-)
&lt;/h2&gt;

&lt;p&gt;Pour cela dans le répertoire "src" à la racine de votre projet, créez un répertoire "Routing", puis créez dans ce dernier une classe "CmsRouteLoader".&lt;/p&gt;

&lt;p&gt;Voici le code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Routing&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Doctrine\ORM\EntityManagerInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Config\Loader\Loader&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Routing\Route&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Routing\RouteCollection&lt;/span&gt;

&lt;span class="n"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CmsRouteLoader&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Loader&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nv"&gt;$isLoaded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/**
     * @var EntityManagerInterface
     */&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;EntityManagerInterface&lt;/span&gt; &lt;span class="nv"&gt;$entityManager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;EntityManagerInterface&lt;/span&gt; &lt;span class="nv"&gt;$entityManager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;entityManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$entityManager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;RouteCollection&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;isLoaded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nf"&gt;RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Do not add the "CMS" loader twice'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RouteCollection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nv"&gt;$nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;entityManager&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;findAll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$nodes&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$defaults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'_controller'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'App\Controller\CmsController::slugs'&lt;/span&gt;
            &lt;span class="p"&gt;];&lt;/span&gt;
            &lt;span class="nv"&gt;$route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$node&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getPath&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nv"&gt;$defaults&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$routes&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cms_slugs_'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$node&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nv"&gt;$route&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;isLoaded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$routes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;supports&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'cms'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$type&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quelques explications (notez que notre classe ressemble beaucoup à la documentation):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;notre classe étend Loader, ce qui va nous permettre de définir une méthode "supports". Vous remarquerez qu'on retourne true si le type du groupe est "cms"....ça vous rappelle le fichier de config ?&lt;/li&gt;
&lt;li&gt;ici la classe est un peu simplifiée, mais en bref, on charge les pages de la base, et pour chaque page on crée dynamiquement une route qu'on ajoute à son tour dans une collection de routes qu'on retourne.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enfin, on enregistre notre service dans les services de l'application en modifiant le fichier config/services.yaml comme suit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/services.yaml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;

    &lt;span class="na"&gt;App\Routing\CmsRouteLoader&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;routing.loader&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On fera quelques modifications dans le prochain chapitre pour mettre en place nos structures pour enregistrer nos pages et autres dans la base de données.&lt;/p&gt;

&lt;p&gt;Il manque aussi le controller 'App\Controller\CmsController' mais pour l'instant on insiste pas, on s'y intéressera plus tard. &lt;/p&gt;

&lt;p&gt;Bientôt la suite ! &lt;/p&gt;

</description>
      <category>symfony</category>
      <category>webdev</category>
      <category>cms</category>
      <category>opensource</category>
    </item>
    <item>
      <title>CMS en Symfony : quelles fonctionnalités ?</title>
      <dc:creator>Guillaume</dc:creator>
      <pubDate>Wed, 23 Feb 2022 22:00:05 +0000</pubDate>
      <link>https://dev.to/gbtux/cms-en-symfony-quelles-fonctionnalites--35ln</link>
      <guid>https://dev.to/gbtux/cms-en-symfony-quelles-fonctionnalites--35ln</guid>
      <description>&lt;p&gt;Bon maintenant qu'on a bien critiqué les CMS et leur stack super lourde, on fait comment ?&lt;br&gt;
Et si on devait faire un CMS, comment on ferait ?&lt;br&gt;
On a besoin de quoi pour faire un CMS ?&lt;/p&gt;

&lt;p&gt;On va essayer de répondre à ce questions dans ce post....&lt;/p&gt;

&lt;h1&gt;
  
  
  Faire le tri dans les fonctionnalités
&lt;/h1&gt;

&lt;p&gt;Voici ce que j'ai retenu des (macros) fonctionnalités de Drupal :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;mise à disposition d'un backoffice de gestion du site&lt;/li&gt;
&lt;li&gt;gestion de l'authentification des contributeurs et administrateurs&lt;/li&gt;
&lt;li&gt;pouvoir créer des types de contenu (dans le backoffice)&lt;/li&gt;
&lt;li&gt;pouvoir créer des contenus (page, article, ...) (dans le backoffice)&lt;/li&gt;
&lt;li&gt;pouvoir gérer des "menus"&lt;/li&gt;
&lt;li&gt;pouvoir modifier le template de la page (dans le code)&lt;/li&gt;
&lt;li&gt;pouvoir créer des blocs de template (dans le code)&lt;/li&gt;
&lt;li&gt;pouvoir créer des vues, transformées soit en bloc, soit en page. Les vues sont une liste de contenus filtrés. &lt;/li&gt;
&lt;li&gt;pouvoir "étendre" la plateforme avec des plugins&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Ce qu'on garde ou pas
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Mise à disposition d'un backoffice de gestion du site
&lt;/h2&gt;

&lt;p&gt;Evidemment on garde. Il faut nécessairement ajouter cette fonctionnalité.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gestion de l'authentification
&lt;/h2&gt;

&lt;p&gt;Idem, on garde, il faut que nos utilisateurs puisse accéder au backoffice et gérer des droits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pouvoir créer des types de contenu
&lt;/h2&gt;

&lt;p&gt;Dans Drupal par exemple, tous les contenus héritent de "Node", le type de base.&lt;/p&gt;

&lt;p&gt;Sur le principe, on va garder la possibilité, évidemment, de pouvoir créer des types dérivés d'un type de base.&lt;br&gt;
Mais laisser cette possibilité en backoffice, j'avoue que je ne comprends pas...&lt;/p&gt;

&lt;p&gt;Quelle est la probabilité de laisser la possibilité à un client final (un contributeur) de créer des types de contenu à la volée ?&lt;br&gt;
C'est complexe pour le modèle en base, nuit donc aux performances, et parfaitement inutile.&lt;/p&gt;

&lt;p&gt;C'est justement ce qui va faire la différence, on va avoir une approche différente.&lt;/p&gt;

&lt;p&gt;Mais sur le fond, on fera ça dans le code, vous verrez pourquoi.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pouvoir créer des contenus
&lt;/h2&gt;

&lt;p&gt;Avoir un backoffice sans pouvoir créer de page ou d'article, ça n'a pas vraiment de sens.&lt;br&gt;
Donc on garde (et on fera même mieux que Drupal ;-) )&lt;/p&gt;

&lt;h2&gt;
  
  
  Pouvoir gérer des "menus"
&lt;/h2&gt;

&lt;p&gt;Pas grand chose à dire, on garde (et on fera plus simple que Drupal)&lt;/p&gt;

&lt;h2&gt;
  
  
  Pouvoir modifier le template de la page
&lt;/h2&gt;

&lt;p&gt;Comme dans beaucoup de CMS, on va garder cette fonctionnalité. Mais aussi la laisser possible uniquement dans le code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pouvoir créer des blocs de template
&lt;/h2&gt;

&lt;p&gt;Les pages à créer, le contenu à créer, est de plus en plus complexe. Donc on est obligé de proposer des mises en pages qui ne sont pas uniquement rigides.&lt;/p&gt;

&lt;p&gt;On va offrir cette possibilité, et elle va même devenir centrale !&lt;br&gt;
Pouvoir créer des pages avec des blocs déjà constitués, pouvoir en créer comme on le veut, y attacher des données, des attributs, on va plutôt aller dans ce sens. &lt;/p&gt;

&lt;h2&gt;
  
  
  Pouvoir créer des vues
&lt;/h2&gt;

&lt;p&gt;On ne garde pas. Trop complexe. On va faire plus simple et plus performant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pouvoir "étendre" la plateforme avec des plugins
&lt;/h2&gt;

&lt;p&gt;Cette fonctionnalité est native dans beaucoup de framework.&lt;br&gt;
Donc il ne sert à rien de réinventer la roue !&lt;/p&gt;

&lt;p&gt;On garde ! Mais on fait différent !&lt;/p&gt;

&lt;p&gt;Dans le prochain épisode, on verra comment on transforme ces fonctionnalités en brique technique.&lt;/p&gt;

&lt;p&gt;Bientôt la suite !   &lt;/p&gt;

</description>
      <category>webdev</category>
      <category>symfony</category>
      <category>cms</category>
      <category>drupal</category>
    </item>
    <item>
      <title>CMS or not CMS ?</title>
      <dc:creator>Guillaume</dc:creator>
      <pubDate>Mon, 21 Feb 2022 22:45:38 +0000</pubDate>
      <link>https://dev.to/gbtux/cms-or-not-cms--1605</link>
      <guid>https://dev.to/gbtux/cms-or-not-cms--1605</guid>
      <description>&lt;p&gt;Cet article est le début d'une série.&lt;/p&gt;

&lt;p&gt;Choix cornélien s'il en est !&lt;/p&gt;

&lt;p&gt;Vous voulez créer une plateforme, mais votre client vous demande une partie applicative et une partie contenu... et là c'est le drame.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclaimer / Parti pris
&lt;/h2&gt;

&lt;p&gt;Oui je suis architecte et développeur Symfony, donc pas beaucoup de mystère. Mais on sera ici loin de la critique.&lt;br&gt;
L'idée n'est pas de critiquer telle ou telle plateforme, mais essayer d'expliquer pourquoi on arrive parfois à se trouver dans des situations complexes, dont il n'y a pas de réponse évidente.&lt;br&gt;
Et puis il faut toujours considérer des paramètres qui peuvent parfois passer au second plan et qui sont pourtant fondamentaux:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;la compétence du ou des développeurs. Si celui ne connait que Drupal (au hasard, mais on va en parler) alors clairement cela peut être le bon choix. Si en revanche c'est un pur développeur Java/Spring, l'effort de prise en main d'un CMS est souvent rédhibitoire; &lt;/li&gt;
&lt;li&gt;l'appétence de ceux-ci pour telle ou telle techno. "J'aime pas PHP", "Moi je fais tout sans framework", etc....ne négligez pas ces aspects.&lt;/li&gt;
&lt;li&gt;les contraintes du client, ou de l'environnement. Environnement 100% Microsoft, ou au contraire 100% Open Source, "PHP, non j'en veux pas !".... et là vos 10 ans d'expérience sur Wordpress s'envolent !&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Une p'tite histoire ?
&lt;/h2&gt;

&lt;p&gt;Avant d'entrer dans le vif du sujet, je vais vous compter 2 histoires.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cas N°1 : client pressé - compétence 0
&lt;/h3&gt;

&lt;p&gt;Un client (interne dans mon cas mais ça ne change rien) vient me voir et me dis : "Guillaume, il faut refaire le site abc.com et tu as 3 mois". Ouch. Le truc est vieux, pas simple et il faut évidemment refaire design et le reste....&lt;br&gt;
C'est 60/70% de contenu mais il y a un gros morceaux autour de la recherche et je n'y connais rien.&lt;br&gt;
Me lancer dans un CMS ? Je n'ai jamais touché ni un Drupal, ni un Wordpress...&lt;br&gt;
Mais bon, en discutant avec le client, on trouve un designer qui me fais des maquettes aux petits oignons et je leur dis que je me charge des modifications de contenu en attendant.&lt;br&gt;
Je code donc des templates Twig en Symfony et la recherche, de la BDD et un ElasticSearch font l'affaire.&lt;br&gt;
On met le tout en prod, tout le monde est content et finalement le contenu n'a pas beaucoup bougé depuis 1 an.....&lt;/p&gt;

&lt;h3&gt;
  
  
  Cas n°2 : figure imposée !
&lt;/h3&gt;

&lt;p&gt;Client n°2 : "Guillaume, puisque tu as fais le premier, le 2ème est à refaire, mais là le client c'est Drupal ou rien".&lt;br&gt;
Effctivement, on a beau négocier, rien à faire....je dois me mettre à Drupal. J'écume Youtube et la doc (horrible) de Drupal. 2ème coup de massue : j'arrive à faire 2 pages simples (Hello world je te hais) mais pas beaucoup d'autres trucs.... et de là à faire un truc "pro" il y a 1 (2 !) pas.&lt;br&gt;
J'arrive à négocier un presta, ultra compétent, qui m'explique sans faire à ma place, mais que c'est lourd !&lt;/p&gt;

&lt;p&gt;Le truc est une usine à gaz !&lt;/p&gt;

&lt;p&gt;Je ne retrouve pas mes repères. C'est à la fois une usine à clics et à la fois un truc dégueu où il faut faire des hooks partout, rajouter des plugins à foison sans trop savoir ce qu'ils font..... expérience de développement horrible !&lt;/p&gt;

&lt;p&gt;Donc dilemme pour la suite, maintenant que je sais faire un peu de CMS. Stop ou encore ?&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi choisir un CMS ?
&lt;/h2&gt;

&lt;p&gt;Alors pourquoi choisir un CMS en 2022 ?&lt;br&gt;
Objectivement, quelques éléments relevés, notamment en discutant avec le client:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;on connait la pile logicielle&lt;/li&gt;
&lt;li&gt;on a déjà ce CMS sur d'autres sites, on maitrise&lt;/li&gt;
&lt;li&gt;le backoffice est déjà fait, on a notre usine à clics&lt;/li&gt;
&lt;li&gt;on peut créer du contenu nous-mêmes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Toutes ces raisons sont sûrement valables, mais honnêtement de moins en moins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;la pile logicielle est remplie de plugins non maîtrisés&lt;/li&gt;
&lt;li&gt;la retro-compatibilité entre les versions n'est souvent pas assurée&lt;/li&gt;
&lt;li&gt;oui le backoffice est déjà présent, mais il contient souvent beaucoup de fonctionnalités dont le client n'a pas besoin&lt;/li&gt;
&lt;li&gt;créer proprement un contenu dans un CMS, mis à part un blog, je demande à voir sans être un spécialiste.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En résumé, la "promesse" des CMS de permettre à un utilisateur non développeur de mettre en ligne du contenu n'est JAMAIS tenue. &lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi ne pas choisir un CMS ?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;La sécurité ! On ne compte plus les CVE sur Wordpress, ou sur Drupal. Que dire quand en plus on lui ajoute 50 plugins contribs ?&lt;/li&gt;
&lt;li&gt;Parce que c'est compliqué ! On passe finalement son temps à casser des comportements natifs du CMS, donc finalement on ...code !&lt;/li&gt;
&lt;li&gt;Qui a déjà laissé créer un type de contenu à un utilisateur en prod ? Du coup à quoi ça sert ????&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On va y revenir, mais cette approche "type de contenu" est assez paradoxale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un headless CMS peut-être ?
&lt;/h2&gt;

&lt;p&gt;Ces dernières années, des tentatives, comme Directus ou Strapi, permettent de créer des types de contenu et des contenus dans un backoffice générique. Sympa, mais du coup, on se retrouve à faire un front office "from scratch". &lt;br&gt;
Là aussi, une approche "data" pure : un contenu = une data d'un type particulier.&lt;br&gt;
C'est bien pour une liste d'articles, mais pas pour bien d'autres chose. Pour des éléments constitués oui, mais sur un site web il n'y a pas que ça !&lt;br&gt;
Ce n'est pas un blog !&lt;/p&gt;

&lt;h2&gt;
  
  
  Et si on le faisait nous-mêmes ?
&lt;/h2&gt;

&lt;p&gt;Un site web aujourd'hui n'est pas juste une liste d'articles et une page "article". ça c'est un blog !&lt;br&gt;
Et c'est bien le problème !&lt;/p&gt;

&lt;p&gt;Nous avons aujourd'hui des pages de plus en plus complexe où il faut gérer des blocs de contenu des complexes mais surtout et souvent non reproductibles.&lt;/p&gt;

&lt;p&gt;En conclusion, on va essayer une nouvelle approche.&lt;br&gt;
Garder ce qui fait la force de notre framework web préféré tout en offrant des possibilités de création de contenu.&lt;/p&gt;

&lt;p&gt;A suivre !&lt;/p&gt;

&lt;p&gt;Dans les prochains épisodes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;exposer le problème&lt;/li&gt;
&lt;li&gt;comment faire ça dans Symfony ? la théorie&lt;/li&gt;
&lt;li&gt;Développement de bundle : BlockMS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La suite &lt;a href="https://dev.to/gbtux/cms-en-symfony-quelles-fonctionnalites--35ln"&gt;ici&lt;/a&gt;&lt;/p&gt;

</description>
      <category>symfony</category>
      <category>cms</category>
      <category>webdev</category>
      <category>drupal</category>
    </item>
    <item>
      <title>Authentification OpenID Connect avec Symfony (3/3)</title>
      <dc:creator>Guillaume</dc:creator>
      <pubDate>Fri, 23 Apr 2021 09:31:53 +0000</pubDate>
      <link>https://dev.to/gbtux/authentification-openid-connect-avec-symfony-3-3-1o22</link>
      <guid>https://dev.to/gbtux/authentification-openid-connect-avec-symfony-3-3-1o22</guid>
      <description>&lt;p&gt;Cet article fait partie d'une série d'articles :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/gbtux/authentification-openid-connect-avec-symfony-3m5h"&gt;Introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/gbtux/authentification-openid-connect-avec-symfony-2-3-218m"&gt;Mettre en place un serveur OpenID Connect avec Keycloak&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Symfony et Keycloak (cet article)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;D'abord "rendons à César ce qui appartient à César" : merci à &lt;a href="https://grafikart.fr/" rel="noopener noreferrer"&gt;Grafikart&lt;/a&gt; pour &lt;a href="https://grafikart.fr/tutoriels/symfony-oauth-authenticator-1362" rel="noopener noreferrer"&gt;son excellent article/vidéo&lt;/a&gt; sur le sujet d'Oauth/OpenID : "Authentification sociale sur Symfony".&lt;/p&gt;

&lt;p&gt;Evidemment, si je fais cet article, c'est qu'il y a une différence! Nous n'utiliserons pas ni Github, ni Google ou autre, mais notre Keycloak, et il y a quelques subtilités qui valent bien un article :D&lt;/p&gt;

&lt;h1&gt;
  
  
  Création du projet
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Note : un projet Symfony 5 est utilisé ici, mais Symfony 4.4 est tout à fait utilisable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Installez la commande &lt;em&gt;symfony&lt;/em&gt; &lt;a href="https://symfony.com/download" rel="noopener noreferrer"&gt;comme expliqué ici&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Puis, créez un projet (ici en version full/application web):&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

symfony new TestKeycloak --full


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Pour avoir une route de test dans notre application, on va créer une simple route /dashboard.&lt;/p&gt;

&lt;p&gt;Dans le répertoire de notre projet, créons le controller:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

bin/console make:controller DashboardController


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;On va laisser le controller par défaut, ça n'a aucune importance pour notre exemple.&lt;/p&gt;

&lt;p&gt;On va aussi laisser le template dans son état initial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration de la base de données
&lt;/h2&gt;

&lt;p&gt;Il faut configurer la base de données. &lt;br&gt;
Personnellement, s'agissant d'un projet de test, je fais ça avec docker et docker-compose pour démarrer une base PostgreSQL.&lt;/p&gt;

&lt;p&gt;Si vous voulez faire comme moi, créez un fichier docker-compose.yaml à la racine de votre projet avec le contenu suivant:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

version: '3'

services:
    database:
        image: postgres:13-alpine
        environment:
            POSTGRES_USER: main
            POSTGRES_PASSWORD: main
            POSTGRES_DB: keycloak
        ports:
            - 5432:5432


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Démarrez la base avec un simple :&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

docker-compose up -d


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Configurez ensuite la base dans le projet en créant le fichier &lt;em&gt;.env.local&lt;/em&gt; à la racine du projet avec le contenu suivant:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

DATABASE_URL="postgresql://main:main@127.0.0.1:5432/keycloak?serverVersion=13&amp;amp;charset=utf8"


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Démarrez le serveur interne à Symfony:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

symfony serve


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Puis rendez-vous dans votre navigateur à l'adresse &lt;a href="http://localhost:8000/dashboard" rel="noopener noreferrer"&gt;http://localhost:8000/dashboard&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notre projet initial est en place, voyons pour la partie authentification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Création d'un utilisateur dans Keycloak
&lt;/h2&gt;

&lt;p&gt;Dans notre cas, nous n'avons pas de fournisseur externe d'identité, comme dans une entreprise, avec un annuaire LDAP ou Active Directory, ou une base de données quelconque.&lt;/p&gt;

&lt;p&gt;Mais pas de soucis, on va pouvoir créer dans notre Keycloak des utilisateurs de test.&lt;/p&gt;

&lt;p&gt;Pour cela, rendez-vous dans l'administration de votre Keycloak : &lt;a href="https://votre_domaine/auth/admin" rel="noopener noreferrer"&gt;https://votre_domaine/auth/admin&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Dans le menu &lt;em&gt;Manage/Users&lt;/em&gt; cliquez sur le bouton "Add user" et créer un utilisateur comme suit:&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuy58jwhl9sgqncauof6v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuy58jwhl9sgqncauof6v.png" alt="Creation utilisateur"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Dans les détails de l'utilisateur créé, allez dans l'onglet "Credentials" et saisissez un mot de passe (avec confirmation) puis cliquez sur "Set password"&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: toutes les manipulations seront faites sur le Realm (Domaine) "Master". Keycloak est complexe, et comporte de multiples fonctionnalités. Lisez la doc ;)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Création d'un client Keycloak
&lt;/h2&gt;

&lt;p&gt;Chaque application "cliente" de Keycloak doit être configurée. &lt;br&gt;
Pour créer un client pour notre application Symfony, allez dans le menu "Clients" dans l'interface d'admin de Keycloak puis sur le bouton "Create".&lt;/p&gt;

&lt;p&gt;Ajoutez les informations comme suit:&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg8r6yo6n1c08uqrfb7mq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg8r6yo6n1c08uqrfb7mq.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;br&gt;
Rien de difficile ici, on donne un nom à notre client, on utilise le protocole OpenID et l'URL de notre projet est bien &lt;a href="http://localhost:8000" rel="noopener noreferrer"&gt;http://localhost:8000&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cliquez sur "save" pour passer au panneau de contrôle de notre client.&lt;/p&gt;

&lt;p&gt;Modifiez les valeurs suivantes :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Consent Required&lt;/strong&gt; doit être à &lt;strong&gt;ON&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Toggle Display client on consent screen&lt;/strong&gt; doit être à &lt;strong&gt;ON&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Toggle Implicit Flow Enabled&lt;/strong&gt; doit être à &lt;strong&gt;ON&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set Access Type&lt;/strong&gt; doit être à &lt;strong&gt;confidential&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Laissez la valeur "Valid redirect URIs" à  &lt;a href="http://localhost:8000/*" rel="noopener noreferrer"&gt;http://localhost:8000/*&lt;/a&gt; pour le moment, même si à terme il faudra sécuriser les URLs de redirection vers notre application Symfony.&lt;/p&gt;

&lt;p&gt;Enregistrez la configuration et basculez sur l'onglet "Credentials".&lt;/p&gt;

&lt;p&gt;Copiez le "secret", et insérez 3 variables d'environnement dans le fichier &lt;em&gt;.env.local&lt;/em&gt; de votre projet Symfony tel que:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

DATABASE_URL="postgresql://main:main@127.0.0.1:5432/keycloak?serverVersion=13&amp;amp;charset=utf8"
KEYCLOAK_SECRET=6b008eb2-e4c8-4afe-8016-cd59f3843d93
KEYCLOAK_CLIENTID=symfony
KEYCLOAK_APP_URL=https://votre_domaine/auth


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Attention : mettez bien "/auth" à la fin de l'URL de votre Keycloak.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Passons maintenant à Symfony.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Attention: je reprécise mon besoin : disposer d'une authentification 100% via Keycloak. Si vous voulez AUSSI disposer d'un formulaire de login en plus de Keycloak, c'est possible ! Allez voir &lt;a href="https://symfony.com/doc/current/security/form_login_setup.html" rel="noopener noreferrer"&gt;la doc officielle de Symfony sur la composant Security&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Mais nous on va faire simple ;)&lt;/p&gt;

&lt;h1&gt;
  
  
  Symfony Security
&lt;/h1&gt;

&lt;p&gt;Le composant Security est un des plus complexes à appréhender. Le composant est complexe, mais il nous facilite grandement la vie, n'oubliez jamais ça ! Combien de fois voyons-nous des applications "faites à la main" et mal/pas  sécurisées !&lt;/p&gt;

&lt;p&gt;Pas de panique, on va y aller pas à pas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Création d'une classe User
&lt;/h2&gt;

&lt;p&gt;Nous avons besoin d'une classe "User" pour 2 raisons :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pouvoir enregistrer cet objet en base de données&lt;/li&gt;
&lt;li&gt;manipuler un objet tout au long de notre processus d'authentification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Encore une fois, Symfony nous facilite la vie avec une simple commande:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

bin/console make:user


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxrwxtlc3eisy1yhmkz66.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxrwxtlc3eisy1yhmkz66.png" alt="make security user"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notre classe utilisateur est maintenant créée, avec son repository, mais nous avons besoin d'un champs supplémentaire : keycloakId.&lt;br&gt;
Ce champs nous servira à associer l'utilisateur Keycloak à notre utilisateur local.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Rien ne vous empêche d'ajouter d'autre champs, puisque on le verra plus tard, Keycloak nous envoie des champs comme le nom que je ne stocke pas dans mon exemple. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Pour ajouter ce champs faites un simple :&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

bin/console make:entity User


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F24o57cfmf30nmc6al5rr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F24o57cfmf30nmc6al5rr.png" alt="ajout keycloakId"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Comme conseillé à la fin de l’exécution de la commande, on va créer un fichier de migration :&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

bin/console make:migration


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd3ckqwkot363ch5k0c4r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd3ckqwkot363ch5k0c4r.png" alt="make migration"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Puis exécuter cette migration:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

bin/console doctrine:migrations:migrate


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Vérifiez dans votre base de données que la table "user" s'est bien créée :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwtxn90nm3n9vvvdx275q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwtxn90nm3n9vvvdx275q.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Ajout des bundles "clients" pour OAuth et Keycloak
&lt;/h2&gt;

&lt;p&gt;Heureusement pour nous, on ne va pas coder toute la partie cliente entre Symfony et keycloak.&lt;br&gt;
Comme d'habitude avec Symfony, il existe des bundles et aujourd'hui 2 vous nous intéresser particulièrement :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;le &lt;a href="https://github.com/knpuniversity/oauth2-client-bundle" rel="noopener noreferrer"&gt;KnpUOAuth2ClientBundle&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;le &lt;a href="https://github.com/stevenmaguire/oauth2-keycloak" rel="noopener noreferrer"&gt;Keycloak Provider&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Le premier est une sorte de coquille vide : si vous regardez la doc, vous verrez que comme Grafikart, vous pouvez choisir un client Github, mais aussi Discord, Facebook, et plein d'autres !&lt;/p&gt;

&lt;p&gt;Le deuxième est donc notre "Provider", l'implémentation spécifique à Keycloak.&lt;/p&gt;

&lt;p&gt;Installons-les :&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

composer require knpuniversity/oauth2-client-bundle
composer require stevenmaguire/oauth2-keycloak


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Oui je sais je fais ça en 2 commandes alors qu'on pourrait faire ça en une ! Pourquoi ? Parce que je n'aime pas installer plusieurs bundles dans la même commande : si l'installation d'un bundle plante c'est plus facile à debugger :-D &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;L'installation du premier bundle va exécuter une "recipe" (recette) pour aller créer un fichier de configuration spécifique : le fichier "config/packages/knpu_oauth2_client.yaml"&lt;/p&gt;

&lt;p&gt;C'est lui qui va nous servir à configurer la connexion à notre Keycloak. Comme nous avons créé des variables d'environnement (dans notre .env.local) ça va être simple :&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

knpu_oauth2_client:
    clients:
        keycloak:
            type: keycloak
            auth_server_url: '%env(KEYCLOAK_APP_URL)%'
            realm: 'master'
            client_id: '%env(KEYCLOAK_CLIENTID)%'
            client_secret: '%env(KEYCLOAK_SECRET)%'
            redirect_route: 'oauth_check'


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Détaillons un peu:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;le nom de notre "client" (ici &lt;strong&gt;keycloak&lt;/strong&gt;) est arbitraire;&lt;/li&gt;
&lt;li&gt;le &lt;strong&gt;type&lt;/strong&gt; en revanche est imposé ;) et c'est pour préciser l'implémentation à utiliser au 1er bundle;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;auth_server_url&lt;/strong&gt; : l'URL de notre Keycloak&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;realm&lt;/strong&gt; : comme dit précédemment, j'utilise le Realm/Domaine de base : "master"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;client_id&lt;/strong&gt; : le nom du client qu'on a créé dans Keycloak&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;client_secret&lt;/strong&gt;: le secret généré par Keycloak&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;redirect_route&lt;/strong&gt;: le nom de la route à appeler sur laquelle Keycloak redirigera après l'authentification. C'est l'URL de "callback" dans la littérature Keycloak (cf explications de la cinématique Keycloak/Oauth dans le chapitre précédent)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pas de panique, cette URL/route n'existe pas encore, mais on va la créer après.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cinématique
&lt;/h2&gt;

&lt;p&gt;Il faut maintenant expliquer comment le processus d'authentification va marcher :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;L'utilisateur essaie de se connecter sur l'application Symfony&lt;/li&gt;
&lt;li&gt;Le firewall de Symfony détecte qu'il n'est pas loggué et le revoit vers l'URL de login de l'application (/oauth/login).&lt;/li&gt;
&lt;li&gt;Le contrôleur Symfony derrière cette URL va "démarrer" le client Keycloak et renvoyer une réponse de redirection à l'utilisateur vers le serveur Keycloak (à partir des informations de la configuration)&lt;/li&gt;
&lt;li&gt;L'utilisateur s'authentifie dans Keycloak et il est alors redirigé vers l'URL de callback (transmise en paramètre de la réponse du .3)&lt;/li&gt;
&lt;li&gt;Le firewall voit la redirection de l'utilisateur après authentification dans Keycloak, vérifie les informations transmises et si elles sont bonnes, authentifie l'utilisateur.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Maintenant qu'on a compris le fonctionnement, il faut l'implémenter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;configurer la sécurité de Symfony pour aller sur "/oauth/login" si on est pas authentifié (en bref, un firewall)&lt;/li&gt;
&lt;li&gt;créer un contrôleur pour implémenter la route "/oauth/login" en redirigeant l'utilisateur&lt;/li&gt;
&lt;li&gt;implémenter un firewall pour l'URL de callback&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Configurer le firewall pour l'URL de login
&lt;/h2&gt;

&lt;p&gt;Symfony et son composant sécurité a déjà beaucoup de mécanismes fournis, dont des "Provider".&lt;br&gt;
On va donc se servir du &lt;em&gt;form_login Authentication Provider&lt;/em&gt; pour rediriger automatiquement l'utilisateur s'il n'est pas authentifié vers notre URL personnalisée.&lt;/p&gt;

&lt;p&gt;La configuration se fait dans config/packages/security.yaml&lt;/p&gt;

&lt;p&gt;Configurez-le comme suit:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

security:
    encoders:
        App\Entity\User:
            algorithm: auto

    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: true
            lazy: true
            form_login:
                login_path: oauth_login

    access_control:
        - { path: ^/dashboard, roles: ROLE_USER }



&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Quelques explications:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;la rubrique "encoders" a été normalement déjà configurée, de même que "providers" par l'utilisation de la commande &lt;em&gt;make:user&lt;/em&gt;. On sait donc quelle est la classe utilisée pour "provider" nos utilisateurs, et qu'on utilise, si on stocke des mots de passe dans la base (donc hors spectre Keycloak), qu'ils seront sécurisés.&lt;/li&gt;
&lt;li&gt;pour les firewalls, dans le main, ajoutez les lignes "form_login" et "login_path: oauth_login". On déclare donc bien une route avec le nom "oauth_login" comme URL de rediection par défaut si on est pas authentifié.&lt;/li&gt;
&lt;li&gt;dans la rubrique "access_control", on a déclaré que toutes les URLs commençant par &lt;em&gt;/dashboard&lt;/em&gt; ont nécessité à être protégées par une authentification et que l'utilisateur authentifié doit à minima posséder le rôle "ROLE_USER".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Une fois cette configuration sauvegardée, continuons en implémentons notre route de nom "oauth_login" et d'URL "/oauth/login".&lt;/p&gt;

&lt;p&gt;Créez un contrôleur:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

bin/console make:controller OAuthController --no-template


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Puis implémentez la route dite comme suit:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

&amp;lt;?php

namespace App\Controller;

use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Client\Provider\KeycloakClient;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Annotation\Route;

class OAuthController extends AbstractController
{
    /**
     * @Route("/oauth/login", name="oauth_login")
     */
    public function index(ClientRegistry $clientRegistry): RedirectResponse
    {
        /** @var KeycloakClient $client */
        $client = $clientRegistry-&amp;gt;getClient('keycloak');
        return $client-&amp;gt;redirect();
    }

    /**
     * @Route("/oauth/callback", name="oauth_check")
     */
    public function check()
    {

    }
}


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Dans la première fonction, comme expliqué dans la cinématique, on récupère le client Keycloak et on redirige l'utilisateur. &lt;/p&gt;

&lt;p&gt;La 2ème fonction est là uniquement pour implémenter l'URL de callback.&lt;br&gt;
Elle est en revanche vide, puisque c'est un autre composant qui va implémenter la logique  et "prendre la main".&lt;/p&gt;

&lt;p&gt;Ce composant, c'est &lt;strong&gt;Symfony Guard&lt;/strong&gt;, et sa classe abstraite &lt;em&gt;AbstractGuardAuthenticator&lt;/em&gt;.&lt;br&gt;
Notre bundle KnpUOAuth2ClientBundle implémente justement une surcouche à ce composant : SocialAuthenticator.&lt;/p&gt;

&lt;p&gt;On va donc pouvoir se servir de cette classe abstraite pour implémenter notre firewall de "callback".&lt;/p&gt;

&lt;p&gt;Dans le projet, créez un répertoire &lt;em&gt;src/Security&lt;/em&gt; et une classe &lt;em&gt;KeycloakAuthenticator&lt;/em&gt; telle que :&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

&amp;lt;?php

namespace App\Security;

use App\Entity\User;
use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;

/**
 * Class KeycloakAuthenticator
 */
class KeycloakAuthenticator extends SocialAuthenticator
{

    private $clientRegistry;
    private $em;
    private $router;

    public function __construct(ClientRegistry $clientRegistry, EntityManagerInterface $em, RouterInterface $router)
    {
        $this-&amp;gt;clientRegistry = $clientRegistry;
        $this-&amp;gt;em = $em;
        $this-&amp;gt;router = $router;
    }
 .......


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Notre classe étends bien le SocialAuthenticator, et on va avoir besoin de:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ClientRegistry: le gestionnaire de clients OAuth&lt;/li&gt;
&lt;li&gt;EntityManagerInterface: pour lire/écrire dans la base de données&lt;/li&gt;
&lt;li&gt;RouterInterface: lire une route/URL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On les injecte dans le constructeur via l'injection de dépendances. Donc pas de configuration particulière à faire dans &lt;em&gt;services.yaml&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Etendre le SocialAuthenticator nous oblige à implémenter un certain nombre de méthodes que voici:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;start: méthode appelée en cas d'erreur si l'authentification n'est pas envoyée dans la requête
Pour nous :
```
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;public function start(Request $request, \Symfony\Component\Security\Core\Exception\AuthenticationException $authException = null)&lt;br&gt;
    {&lt;br&gt;
        return new RedirectResponse(&lt;br&gt;
            '/oauth/login', // might be the site, where users choose their oauth provider&lt;br&gt;
            Response::HTTP_TEMPORARY_REDIRECT&lt;br&gt;
        );&lt;br&gt;
    }&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
* supports: méthode appelée sur toutes les requêtes pour savoir si on déclenche cet Authenticator ou pas.
Pour nous:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;public function supports(Request $request)&lt;br&gt;
    {&lt;br&gt;
        return $request-&amp;gt;attributes-&amp;gt;get('_route') === 'oauth_check';&lt;br&gt;
    }&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
* getCredentials: détermine comment on récupère les informations d'authentification dans la requête pour les passer en paramètre de la fonction *getUser*
Pour nous:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;public function getCredentials(Request $request)&lt;br&gt;
    {&lt;br&gt;
        return $this-&amp;gt;fetchAccessToken($this-&amp;gt;getKeycloakClient());&lt;br&gt;
    }&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
* getUser : c'est LA fonction de l'authenticator : comment récupérer l'utilisateur. Ici, on a 3 possibilités :
    - soit l'utilisateur existe et s'est déjà connecté avec Keycloak
    - soit l'utilisateur existe dans la base mais ne s'est jamais connecté avec Keycloak
    - soit l'utilisateur n'existe pas du tout et on le crée

Ainsi:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;public function getUser($credentials, \Symfony\Component\Security\Core\User\UserProviderInterface $userProvider)&lt;br&gt;
    {&lt;br&gt;
        $keycloakUser = $this-&amp;gt;getKeycloakClient()-&amp;gt;fetchUserFromToken($credentials);&lt;br&gt;
        //existing user ?&lt;br&gt;
        $existingUser = $this&lt;br&gt;
                            -&amp;gt;em&lt;br&gt;
                            -&amp;gt;getRepository(User::class)&lt;br&gt;
                            -&amp;gt;findOneBy(['keycloakId' =&amp;gt; $keycloakUser-&amp;gt;getId()]);&lt;br&gt;
        if ($existingUser) {&lt;br&gt;
            return $existingUser;&lt;br&gt;
        }&lt;br&gt;
        // if user exist but never connected with keycloak&lt;br&gt;
        $email = $keycloakUser-&amp;gt;getEmail();&lt;br&gt;
        /** &lt;a class="mentioned-user" href="https://dev.to/var"&gt;@var&lt;/a&gt; User $userInDatabase */&lt;br&gt;
        $userInDatabase = $this-&amp;gt;em-&amp;gt;getRepository(User::class)&lt;br&gt;
            -&amp;gt;findOneBy(['email' =&amp;gt; $email]);&lt;br&gt;
        if($userInDatabase) {&lt;br&gt;
            $userInDatabase-&amp;gt;setKeycloakId($keycloakUser-&amp;gt;getId());&lt;br&gt;
            $this-&amp;gt;em-&amp;gt;persist($userInDatabase);&lt;br&gt;
            $this-&amp;gt;em-&amp;gt;flush();&lt;br&gt;
            return $userInDatabase;&lt;br&gt;
        }&lt;br&gt;
        //user not exist in database&lt;br&gt;
        $user = new User();&lt;br&gt;
        $user-&amp;gt;setKeycloakId($keycloakUser-&amp;gt;getId());&lt;br&gt;
        $user-&amp;gt;setEmail($keycloakUser-&amp;gt;getEmail());&lt;br&gt;
        $user-&amp;gt;setRoles(['ROLE_USER']);&lt;br&gt;
        $this-&amp;gt;em-&amp;gt;persist($user);&lt;br&gt;
        $this-&amp;gt;em-&amp;gt;flush();&lt;br&gt;
        return $user;&lt;br&gt;
    }&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
* onAuthenticationFailure: message renvoyé quand l'authentification échoue
Pour nous:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;public function onAuthenticationFailure(Request $request, \Symfony\Component\Security\Core\Exception\AuthenticationException $exception)&lt;br&gt;
    {&lt;br&gt;
        $message = strtr($exception-&amp;gt;getMessageKey(), $exception-&amp;gt;getMessageData());&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    return new Response($message, Response::HTTP_FORBIDDEN);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
* onAuthenticationSuccess: que se passe-t-il après l'authentification ? Ici on redirige vers la page /dashboard
Pour nous:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;public function onAuthenticationSuccess(Request $request, \Symfony\Component\Security\Core\Authentication\Token\TokenInterface $token, string $providerKey)&lt;br&gt;
    {&lt;br&gt;
        // change "app_homepage" to some route in your app&lt;br&gt;
        $targetUrl = $this-&amp;gt;router-&amp;gt;generate('dashboard');&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    return new RedirectResponse($targetUrl);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Et une fonction de récupération du client à partir du clientRegistry injecté dans le constructeur:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;/**&lt;br&gt;
     * @return \KnpU\OAuth2ClientBundle\Client\Provider\KeycloakClient&lt;br&gt;
     */&lt;br&gt;
    private function getKeycloakClient()&lt;br&gt;
    {&lt;br&gt;
        return $this-&amp;gt;clientRegistry-&amp;gt;getClient('keycloak');&lt;br&gt;
    }&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Voilà ! Notre Authenticator est maintenant créé.
Pour l'utiliser, modifions encore notre fichier de configuration de la sécurité (config/packages/security.yaml):

![utilisation du Guard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3wkfkkbek34vic5crsx8.png)

Et voilà ! Testons maintenant !

Démarrons notre serveur Symfony:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;symfony serve&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Puis ouvrez votre navigateur à l'adresse [http://localhost:8000/dashboard](http://localhost:8000/dashboard).

Vous êtes automatique redirigé sur le serveur Keycloak:
![login keycloak](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6w8o5s4fyubdp9thghvz.png)

Puis Keycloak vous demande votre consentement sur les données partagées avec l'application Symfony:
![Consentement](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kg7gku24vf2b6dki9ivv.png)

Après consentement, on revient bien sur notre page dashboard ....AUTHENTIFIE !

![YEEEESSS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eqfy80qlxavpx52by49z.png)

Voilà un petit aperçu des possibilités de Keycloak et de son intégration avec Symfony.

Des questions ? Des remarques ? N'hésitez pas !
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>symfony</category>
      <category>keycloak</category>
      <category>openid</category>
      <category>php</category>
    </item>
    <item>
      <title>Authentification OpenID Connect avec Symfony (2/3)</title>
      <dc:creator>Guillaume</dc:creator>
      <pubDate>Tue, 20 Apr 2021 11:03:55 +0000</pubDate>
      <link>https://dev.to/gbtux/authentification-openid-connect-avec-symfony-2-3-218m</link>
      <guid>https://dev.to/gbtux/authentification-openid-connect-avec-symfony-2-3-218m</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Cet article fait partie d'une série d'articles :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/gbtux/authentification-openid-connect-avec-symfony-3m5h"&gt;Introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Mettre en place un serveur OpenID Connect avec Keycloak (cet article)&lt;/li&gt;
&lt;li&gt;Symfony et Keycloak&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Après cette (longue) introduction, on va enfin rentrer dans le vif du sujet. &lt;br&gt;
Aujourd'hui c'est Keycloak!&lt;br&gt;
On va l'installer en mode "standalone", avec une base Mysql (ou MariaDB, suivant votre choix).&lt;br&gt;
On mettra enfin un serveur Apache devant, configuré en SSL et en reverse proxy.&lt;/p&gt;

&lt;p&gt;Vous êtes prêts ? C'est parti !&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Notes: Comme dit précédemment, Keycloak sera installé sur une VM. On va imaginer un serveur sur le domaine "gbtux.org".&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;
  
  
  Installation de Keycloak
&lt;/h1&gt;

&lt;p&gt;Connectez-vous à votre VM, en root, et allez dans le répertoire /opt.&lt;/p&gt;
&lt;h2&gt;
  
  
  Installation de Java
&lt;/h2&gt;

&lt;p&gt;Keycloak nécessite Java pour fonctionner.&lt;br&gt;
On va donc l'installer.&lt;br&gt;
Mettez donc votre liste de paquets à jour :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apt-get update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Puis installez le JDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apt install default-jdk -y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Contrôlez la bonne exécution de Java via :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;java -version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Téléchargement
&lt;/h2&gt;

&lt;p&gt;Téléchargez le sources de keycloak avec wget :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wget https://github.com/keycloak/keycloak/releases/download/12.0.4/keycloak-12.0.4.tar.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Puis, décompressez l'archive avec :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tar zxvf keycloak-12.0.4.tar.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vous avez maintenant un répertoire /opt/keycloak-12.0.4&lt;/p&gt;

&lt;h2&gt;
  
  
  Création d'un utilisateur
&lt;/h2&gt;

&lt;p&gt;Pour faire ça bien, on va créer un utilisateur et un groupe associé.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;groupadd keycloak
useradd -r -g keycloak -d /opt/keycloak -s /sbin/nologin keycloak
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Changement des permissions sur le répertoire Keycloak
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ sudo chown -R keycloak: keycloak
$ sudo chmod o+x /opt/keycloak/bin/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Keycloak en tant que service (avec System D)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fichier de config
&lt;/h3&gt;

&lt;p&gt;D'abord, on va créer un répertoire qui va accueillir  notre fichier de configuration&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir /etc/keycloak
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Puis on va copier le fichier contenu dans la distribution de keycloak.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cp /opt/keycloak-12.0.4/docs/contrib/scripts/systemd/wildfly.conf keycloak.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pas besoin de l'adapter, on va laisser les valeurs par défaut (fichier de configuration XML, mode et adresse d'écoute)&lt;/p&gt;

&lt;h3&gt;
  
  
  Script de démarrage
&lt;/h3&gt;

&lt;p&gt;De la même façon, on va se servir d'un fichier de la distribution de Keycloak:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd /opt/keycloak-12.0.4/bin
cp ../docs/contrib/scripts/systemd/launch.sh .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Editez le nouveau fichier pour modifier la variable WILDFLY_HOME comme suit :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh52x8hz47iu2eifwvjhc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh52x8hz47iu2eifwvjhc.png" alt="Fichier launch"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Fichier de service
&lt;/h3&gt;

&lt;p&gt;Maintenant qu'on s'est occupé du fichier de configuration et du script de démarrage, mettons tout cela en musique pour pouvoir démarrer Keycloak comme un véritable service.&lt;/p&gt;

&lt;p&gt;Pour cela, encore une fois, la distribution de Keycloak nous fournit tout ce qu'il faut.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cp /opt/keycloak-12.0.4/docs/contrib/scripts/systemd/wildfly.service /etc/systemd/system/keycloak.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Maintenant, éditez le fichier copié (/etc/systemd/system/keycloak.service) et adaptez-le comme suit:&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foxr6ycg88rdkx1qaigtp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foxr6ycg88rdkx1qaigtp.png" alt="keycloak.service"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tout est maintenant en place pour prendre les modifications en compte par systemd:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;systemctl daemon-reload
systemctl enable keycloak
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;et lancer notre nouveau service&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;systemctl start keycloak
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Contrôlez son démarrage avec un simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;systemctl status keycloak
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq9ysyfgqc9jg0u4oeo35.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq9ysyfgqc9jg0u4oeo35.png" alt="status keycloak"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Contrôlez aussi l'accès à l'interface d'admin de Keycloak à l'adresse http://&amp;lt;nom_machine_public_de_la_vm:8080/auth/ (dans notre exemple : &lt;a href="http://gbtux.org:8080/auth/" rel="noopener noreferrer"&gt;http://gbtux.org:8080/auth/&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fblefu3lu3cfonw9f1xqe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fblefu3lu3cfonw9f1xqe.png" alt="capture keycloak"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Keycloak en SSL
&lt;/h2&gt;

&lt;p&gt;Pour mettre notre serveur Keycloak en SSL (httpS ;) ), il nous faut un certificat.&lt;br&gt;
On va utiliser &lt;em&gt;certbot&lt;/em&gt;, l'outil de &lt;a href="https://letsencrypt.org/fr/" rel="noopener noreferrer"&gt;Let's Encrypt&lt;/a&gt; qui permet de créer des certificats automatiquement.&lt;/p&gt;

&lt;p&gt;Installons-le :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apt install certbot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Puis créons notre certificat :&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Attention: certbot a besoin du port 80 pour fonctionner. Or, notre serveur Apache tourne sur ce port. Arrêtez-le temporairement avec un simple &lt;em&gt;systemctl stop apache2&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;certbot certonly --standalone -d gbtux.org
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vous pouvez constater que certbot a créé toute une arborescence dans &lt;em&gt;/etc/letsencrypt&lt;/em&gt;, et notamment un répertoire /etc/letsencrypt/live/. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On va aussi utiliser tous ces fichiers *.pem dans notre configuration Apache.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Maintenant que nous avons notre certificat, on va l'importer dans le &lt;em&gt;keystore&lt;/em&gt; (littéralement &lt;em&gt;magasin de clés&lt;/em&gt;) de Keycloak.&lt;/p&gt;

&lt;p&gt;Allons dans le répertoire de configuration où se trouve notre keystore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd /opt/keycloak-12.0.4/standalone/configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dans ce répertoire se trouve un fichier &lt;em&gt;application.keystore&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;Importons notre certificat (évidemment, remplacez tous les "gbtux.org" par votre nom de domaine):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openssl pkcs12 -export -inkey /etc/letsencrypt/live/gbtux.org/privkey.pem -in /etc/letsencrypt/live/gbtux.org/fullchain.pem -out application.keystore -name gbtux.org
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;RETENEZ BIEN LE MOT DE PASSE QUE VOUS ALLEZ CREER !&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Vérifions l'import (le mot de passe demandé est évidemment celui que vous venez de créer):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;keytool -list -v -keystore application.keystore 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configurons Keycloak pour utiliser ce nouveau certificat ajouté dans le keystore.&lt;br&gt;
Dans le même répertoire, éditez le fichier &lt;em&gt;standalone.xml&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Repérez l'entrée "security-realms" puis celle concernat "ApplicationRealm" (aux alentours de la ligne 46) et modifiez les valeurs de configuration tel que:&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0463x7irn1we9pwdgwyk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0463x7irn1we9pwdgwyk.png" alt="ssl realm"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Redémarrez Keycloak pour prendre en compte la configuration&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;systemctl restart keycloak
systemctl status keycloak
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et admirez le résultat ;) : &lt;a href="https://votre_domaine:8443/auth/" rel="noopener noreferrer"&gt;https://votre_domaine:8443/auth/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Créer un utilisateur d'admin
&lt;/h2&gt;

&lt;p&gt;Maintenant qu'on a accès à la console, créez un utilisateur d'admin/&lt;/p&gt;

&lt;p&gt;Placez-vous dans le répertoire bin de Keycloak:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd /opt/keycloak-12.0.4/bin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Puis créez votre utilisateur (et redémarrez Keycloak):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./add-user-keycloak.sh -r master -u &amp;lt;username&amp;gt; -p &amp;lt;password&amp;gt;
systemctl restart keycloak
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cliquez sur le lien "Administration Console" sur la page d'accueil de votre Keycloak, et testez votre login !&lt;/p&gt;

&lt;h1&gt;
  
  
  Apache
&lt;/h1&gt;

&lt;p&gt;Maintenant que notre Keycloak marche en SSL, on va mettre un serveur Apache devant en reverse proxy. Pourquoi me direz-vous ? ben ...parce que c'est mieux de ne pas directement exposer son serveur d'application. &lt;br&gt;
La bonne pratique c'est bien de mettre un Apache en frontal qui relaie les requêtes externes vers un serveur d'application qui lui n'écoute que sur une adresse "locale". Cela vous permettrais de filtrer l'accès à la partie admin de Keycloak par exemple&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Notez qu'ici je vous fais faire les 2 (port 443 via Apache, port 8443 en &lt;em&gt;direct&lt;/em&gt;) pour éviter de complexifier la configuration. Pour &lt;em&gt;faire ça bien&lt;/em&gt;, il faut modifier le fichier de configuration de Keycloak (/etc/keycloak/keycloak.conf et changer le paramètre WILDFLY_BIND pour 127.0.0.1). Ainsi, le Keycloak n'est plus accessible que via Apache (i.e le port 8443 n'est plus accessible depuis une IP publique).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Installons Apache HTTPD:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apt install apache2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Activer les modules nécessaires
&lt;/h2&gt;

&lt;p&gt;Pour faire un reverse proxy, Apache a besoin de quelques modules activés&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a2enmod proxy proxy_http ssl headers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pour les prendre en compte, redémarrez Apache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;systemctl restart apache2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Création d'un VirtualHost
&lt;/h2&gt;

&lt;p&gt;On va créer un VirtualHost dédié à notre Keycloak, pour y mettre un &lt;em&gt;reverse proxy&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Créez un fichier de configuration dans la configuration Apache :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/apache2/sites-available/keycloak.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Puis déclarons notre configuration à l'intérieur:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;IfModule mod_ssl.c&amp;gt;
    &amp;lt;VirtualHost _default_:443&amp;gt;

        ErrorLog ${APACHE_LOG_DIR}/keycloak-error.log
        CustomLog ${APACHE_LOG_DIR}/keycloak-access.log combined

        SSLEngine on
        SSLCertificateFile  /etc/letsencrypt/live/gbtux.org/cert.pem
        SSLCertificateKeyFile /etc/letsencrypt/live/gbtux.org/privkey.pem
        SSLCertificateChainFile /etc/letsencrypt/live/gbtux.org/chain.pem

        ProxyPreserveHost On
            SSLProxyEngine On
            SSLProxyCheckPeerCN on
            SSLProxyCheckPeerExpire on
            RequestHeader set X-Forwarded-Proto "https"
            RequestHeader set X-Forwarded-Port "443"
            ProxyPass / https://127.0.0.1:8443/
            ProxyPassReverse / https://127.0.0.1:8443/
    &amp;lt;/VirtualHost&amp;gt;
&amp;lt;/IfModule&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Activez maintenant ce nouveau VirtualHost :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;systemctl restart apache2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Puis testez si ça marche dans votre navigateur à l'adresse &lt;a href="https://votre_domaine/auth/" rel="noopener noreferrer"&gt;https://votre_domaine/auth/&lt;/a&gt; &lt;br&gt;
 (&lt;a href="https://gbtux.org/auth/" rel="noopener noreferrer"&gt;https://gbtux.org/auth/&lt;/a&gt; pour mon exemple).&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Une grosse étape a été franchie. Notre serveur d'authentification est installé et configuré proprement.&lt;/p&gt;

&lt;p&gt;Dans le prochain épisode, on fait du Symfony !&lt;/p&gt;

</description>
      <category>php</category>
      <category>symfony</category>
      <category>openid</category>
      <category>keycloak</category>
    </item>
    <item>
      <title>Authentification OpenID Connect avec Symfony (1/3)</title>
      <dc:creator>Guillaume</dc:creator>
      <pubDate>Sun, 18 Apr 2021 14:47:52 +0000</pubDate>
      <link>https://dev.to/gbtux/authentification-openid-connect-avec-symfony-3m5h</link>
      <guid>https://dev.to/gbtux/authentification-openid-connect-avec-symfony-3m5h</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Cet article fait partie d'une série d'articles :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Introduction (cet article)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/gbtux/authentification-openid-connect-avec-symfony-2-3-218m"&gt;Mettre en place un serveur OpenID Connect avec Keycloak&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Symfony et Keycloak&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;Après pas mal de recherches sur le sujet et quelques mises en place avec d'autres framework (Node au hasard), on va aujourd'hui s'attaquer à un sujet à la fois simple fonctionnellement mais plus complexe techniquement : "Comment mettre en place un SSO (Single-Sign-On) dans mes applications Symfony ?".&lt;/p&gt;

&lt;p&gt;On ne va pas se mentir. Rentrer 20 mots de passe différents pour chaque application de son SI, c'est acceptable pour des applications "publiques" sur Internet, mais pas en entreprise où on a envie de fluidité, surtout sur un réseau "fermé".&lt;/p&gt;

&lt;p&gt;Certains l'ont bien compris, comme Google, avec une authentification unique quelle que soit l'application utilisée dans l'univers Google.&lt;/p&gt;

&lt;p&gt;Pourquoi OpenID Connect et pas une autre méthode ?&lt;br&gt;
Parce que c'est celle utilisée par de nombreux acteurs (Github, Twitter, Google, ....), et qu'en quelques années c'est devenu le standard de fait.&lt;/p&gt;

&lt;p&gt;Elle offre beaucoup d'avantages, dont celui d'être exclusivement basée sur le protocole HTTP.&lt;/p&gt;

&lt;h1&gt;
  
  
  Quelles briques techniques ?
&lt;/h1&gt;

&lt;p&gt;On a seulement 2 briques ici:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;une brique Symfony, notre application à sécuriser ("fournisseur de service" pour OpenID)&lt;/li&gt;
&lt;li&gt;une brique "serveur SSO". On va voir que cette brique est en fait 3 briques fonctionnelles, résumées souvent à une seule brique technique. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Je vous recommande &lt;a href="https://www.ssi.gouv.fr/uploads/2020/09/anssi-guide-recommandations_pour_la_securisation_de_la_mise_en_oeuvre_du_protocole_openid_connect-v1.0.pdf"&gt;l'excellent guide de l'ANSSI&lt;/a&gt;. LISEZ-LE ! &lt;/p&gt;

&lt;p&gt;Mais voici en gros comment ça marche ;)&lt;/p&gt;

&lt;p&gt;Comme expliqué précédemment, nos 2 briques vont discuter ensemble pour authentifier un utilisateur. &lt;/p&gt;

&lt;p&gt;Mais en fait la brique "d'authentification" a 3 rôles :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fournisseur d'identité : qui est l'utilisateur ? ses informations ? Nom, prénom, username, etc.....&lt;/li&gt;
&lt;li&gt;Authentification: Demander à l'utilisateur de s'authentifier par un moyen X (login/mot de passe dans une base de données, un LDAP/AD, mais aussi via son token Kerberos, etc....)&lt;/li&gt;
&lt;li&gt;Autoriser: OK l'utilisateur existe, il s'est authentifié, mais a-t-il le droit d'accéder à l'application ?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nous allons utiliser Keycloak, de Redhat (open source).&lt;br&gt;
C'est là-aussi un des standards du marché. Il réunit bien les 3 rôles de la brique SSO, et peut marcher dans différentes configurations.&lt;/p&gt;

&lt;p&gt;S'agissant d'une brique centrale d'un système d'information, il ne faut pas faire n'importe quoi. Certes la redonder, mais aussi la mettre dans 2 datacenter différents. Keycloak vous offre ainsi plusieurs "Operating Mode" (&lt;a href="https://www.keycloak.org/docs/latest/server_installation/#_operating-mode"&gt;cf la doc&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Pas de panique, nous on va faire simple : &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;une VM qui hébergera Keycloak (avec un Apache devant, on y reviendra), en mode "standalone"&lt;/li&gt;
&lt;li&gt;une machine de développement pour l'application Symfony.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Et c'est tout !&lt;/p&gt;

&lt;h1&gt;
  
  
  Heu, rappelle-moi déjà comment ça marche OpenID Connect ?
&lt;/h1&gt;

&lt;p&gt;C'est simple ! (ou pas)&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;L'utilisateur ouvre son navigateur et appelle l'URL de l'application.&lt;/li&gt;
&lt;li&gt;L'application regarde la requête et constate qu'elle n'est pas une requête authentifiée.&lt;/li&gt;
&lt;li&gt;Pas de panique, l'application renvoie une requête de redirection (HTTP 302) vers le serveur SSO (avec quelques informations en plus, dont l'URL de redirection, un "client_id", etc...)&lt;/li&gt;
&lt;li&gt;le navigateur de l'utilisateur, voyant la réponse de l'application (redirection), ouvre l'URL renvoyée, celle du serveur SSO.&lt;/li&gt;
&lt;li&gt;Le serveur SSO (Keycloak) présente alors une fenêtre de login à l'utilisateur (cas le plus commun)&lt;/li&gt;
&lt;li&gt;Après la saisie du login/mot de passe par l'utilisateur, le SSO va vérifier si l'utilisateur existe, si les login/mdp sont bons, si l'utilisateur a les droits d'appeler l'application, et enfin renvoyer à son tour une requête de redirection (HTTP 302), agrémentée évidemment des informations de l'utilisateur demandées (le "scope").&lt;/li&gt;
&lt;li&gt;Après cette 2ème requête de redirection, le navigateur appelle une 2ème fois l'application, mais cette fois-ci avec un "token" lui permettant de s'authentifer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Et voilà, c'est aussi "simple" que cela :-D &lt;/p&gt;

&lt;p&gt;L'aventure vous tente, alors rendez-vous dans les prochains épisodes ! On va parler technique !&lt;/p&gt;

</description>
      <category>symfony</category>
      <category>keycloak</category>
      <category>openid</category>
      <category>php</category>
    </item>
  </channel>
</rss>
