The Hypermedia Driven Application (HDA) architecture is a synthesis of two preceding architectures: the original Multi-Page Application (MPA) architecture and the (relatively) newer Single-Page Application (SPA) architecture. It attempts to capture the advantages of both: the simplicity and reliability of MPAs, with a REST-ful Architecture that uses Hypermedia As The Engine Of Application State, while providing a better user experience, on par with SPAs in many cases.
Hotwire : HTML over the wire
Plutôt que de mettre en place un framework Javascript complexe tel que React, Vue, Angular, il est tout à fait possible de gérer le frontend d'une application web avec des technologies beaucoup plus simples. Les outils présentés ici permettent de créer des interfaces modernes, maintenables par un développeur seul ou une petite équipe. Ils sont utilisables directement en HTML, le but étant d'abstraire le Javascript et le CSS.
HTML + Tailwind CSS + Htmx + Alpine.js = ❤️
L'idée est de revenir aux technologies de base du web que sont HTML et CSS afin de concevoir des sites plus réactifs, plus écoresponsables et plus durables. L'idéal étant de faire des sites statiques en optimisant les requêtes et les échanges de données avec le serveur et en limitant l'utilisation du Javascript.
HTML + Simple.css = ❤️❤️
Dans une démarche plus radicale, le protocole Gemini est encore plus minimaliste.
Gemtext = ❤️❤️❤️
Tailwind CSS
Tailwind est un framework CSS basé sur des classes utilitaires plutôt que des classes par composant.
Les concepts du CSS doivent être assimilés puisqu'on les retrouve dans Tailwind. Les layouts flexbox et grid permettant de positionner les éléments sont particulèrement importants.
Installation
La procédure d'installation détaillée ici concerne Django. Pour l'installation dans un autre environnement se référer à la documentation officielle.
Module django-tailwind
Le module django-tailwind nécessite l'installation préalable de Node.js.
1. Installation du module :
pip install django-tailwind
2. Ajout de l'app tailwind
dans settings.xml
:
INSTALLED_APPS = [
...
'tailwind',
]
3. Initialisation de Tailwind :
une configuration et une feuille de style minimales sont créées dans theme/static_src/tailwind.config.js
et theme/static_src/src/style.css
.
python manage.py tailwind init
4. Ajout de l'app theme
dans settings.xml
:
INSTALLED_APPS = [
...
'tailwind',
'theme',
]
TAILWIND_APP_NAME = 'theme'
INTERNAL_IPS = [
"127.0.0.1",
]
5. Installation des dépendance de Tailwind : installe les dépendances Javascript avec npm.
python manage.py tailwind install
6. Démarrage de Tailwind :
surveille les modifications de classes dans les fichiers HTML et génère à la volée la feuille de style dans theme/static/css/dist/styles.css
.
python manage.py tailwind start
7. Déploiement de Tailwind :
compile et minimise la feuille de style pour la production dans theme/static/css/dist/styles.css
.
python manage.py tailwind build
8. Ajout de la feuille de style Tailwind dans le template de base :
{% load tailwind_tags %}
...
<head>
...
{% tailwind_css %}
...
</head>
Module pytailwindcss
Le module pytailwindcss
ne dépend pas de Node.js, il utilise le client standalone officiel.
1. Installation du module :
pip install pytailwindcss
2. Téléchargement du client :
tailwindcss
3. Initialisation de la configuration tailwind.config.js
:
tailwindcss init
4. Initialisation de la feuille de style minimale tailwind.css
:
@tailwind base;
@tailwind components;
@tailwind utilities;
5. Démarrage de Tailwind : surveille les modifications de classes dans les fichiers HTML et génère à la volée la feuille de style.
tailwindcss -i tailwind.css -o static/style.css --watch
6. Déploiement de Tailwind : compile et minimise la feuille de style pour la production.
tailwindcss -i tailwind.css -o static/style.css --minify
7. Ajout de la feuille de style Tailwind dans le template de base :
{% load static %}
...
<head>
...
<link rel="stylesheet" href="{% static 'style.css' %}">
...
</head>
Le fichier
tailwind.config.js
contient la configuration : fichiers analysés, theme, plugins…Le fichier
tailwind.css
contient les styles personnalisés : directives@tailwind
,@layer
,@apply
…
Utilisation
Les sources sont disponibles sur sourcehut.
Il suffit d'utiliser les classes utilitaires fournies par Tailwind directement dans les fichiers HTML. Il en existe de nombreuses qui sont listées dans la documentation. Voici quelques exemples :
<body class="bg-gray-100 flex flex-col xl:flex-row">
<h1 class="text-blue-400">Lorem ipsum</h1>
<p class="text-white bg-indigo-900">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<input type="text" class="bg-transparent border border-red-600" placeholder="…">
<p class="text-md font-mono font-bold">Hello World</p>
<p class="text-lg font-sans line-through">Hello World</p>
<p class="text-4xl font-serif text-center">Hello World</p>
<p class="text-6xl font-mono text-right italic font-extrabold">Hello World</p>
<div class="bg-red-700 h-10 w-auto mr-10"></div>
<div class="bg-green-700 h-16 w-4/6 my-10"></div>
<div class="bg-blue-700 h-24 w-6/12 mx-auto"></div>
<div class="border border-pink-400 h-40 w-56 mt-10 mx-auto">
<h2 class="text-pink-700 p-16">Lorem ipsum</h2>
</div>
<nav class="flex justify-between items-center px-16 bg-purple-800 text-white h-24">
<h1>Title</h1>
<ul class="flex justify-between w-56">
<a href="#">
<li>Link</li>
</a>
<a href="#">
<li>Link</li>
</a>
<a href="#">
<li>Link</li>
</a>
</ul>
</nav>
<div class="h-32 w-32 mt-16 mx-auto lg:bg-orange-500 md:bg-red-500 sm:bg-purple-800"></div>
<div class="h-32 w-32 mt-16 mx-auto lg:bg-orange-500 md:bg-green-800 sm:bg-indigo-300"></div>
<div class="h-32 w-32 mt-16 mx-auto lg:bg-orange-500 md:bg-blue-200 sm:bg-teal-600"></div>
</body>
Astuce : il existe des bibliothèques de composants prêts à l'emploi : TailwindUI, daisyUI, Tailblocks.
Simple.css
Simple.css est un framework CSS sans classe. Il permet de créer très rapidement des sites simples tels que des blogs ou des pages de présentation. Il ne contient pas de classes CSS, on code uniquement avec du HTML sémantique tout en profitant de fonctionnalités améliorées :
- styles for HTML elements
- default fonts
- responsive design
- dark mode
La feuille de style est très légère et peut être étendue en ajoutant une feuille de style personnalisée dans le head
de la page :
<link rel="stylesheet" href="simple.min.css">
<link rel="stylesheet" href="simple.sub.css">
Le présent site utilise Simple.css et des pages HTML classiques.
Htmx
Htmx est une bibliothèque donnant accès à des fonctionnalités modernes du navigateur (AJAX, CSS Transitions, WebSockets, Server Sent Events) directement en HTML plutôt qu'en Javascript.
Installation
L'installation consiste à copier la librairie et l'inclure dans le head
de la page avec une balise script
:
<script defer src="/path/to/htmx.min.js"></script>
La configuration peut ensuite se faire avec une balise meta
:
<meta name="htmx-config" content='{"defaultSwapStyle":"outerHTML"}'>
Se référer à la documentation et à la référence.
Utilisation
Htmx envoie des requêtes AJAX et attend en retour des réponses HTML (fragments HTML). Htmx échange ensuite le HTML retourné avec la cible indiquée dans la page.
Request : définition de la requête (type et URL).
<!-- hx-get, hx-post, hx-put, hx-patch, hx-delete -->
<a hx-get="/link">Link</a>
Trigger : évènement déclenchant la requête.
<!-- hx-trigger -->
<div hx-get="/clicked" hx-trigger="click">Click Me</div>
<div hx-get="/clicked" hx-trigger="click[ctrlKey]">Click Me</div>
<input hx-get="/search" hx-trigger="keyup changed delay:500ms"/>
<div hx-get="/news" hx-trigger="every 3s"></div>
<div hx-get="/messages" hx-trigger="load delay:1s"></div>
Indicator : indicateur de requête en cours (icône de chargement).
<!-- hx-indicator -->
<button hx-get="/click">
Click Me!
<img class="htmx-indicator" src="/spinner.svg">
</button>
<button hx-get="/click" hx-indicator="#indicator">
Click Me!
</button>
<img id="indicator" class="htmx-indicator" src="/spinner.svg"/>
Target : élément du DOM qui sera remplacé par le HTML retourné (élément portant la requête par défaut).
<!-- hx-target -->
<a hx-get="/link" hx-target="#results">Link</a>
<div id="results"></div>
Swapping : stratégie de remplacement entre la réponse et la cible d'une requête.
<!-- hx-swap -->
<div hx-get="/example" hx-swap="afterend"></div>
<div hx-get="/example" hx-swap="innerHTML swap:1s"></div>
<div hx-get="/example" hx-swap="beforeEnd scroll:bottom"></div>
<div hx-get="/example" hx-swap="innerHTML show:top" hx-target="#another-div"></div>
Synchronization : synchronisation de requêtes AJAX entre plusieurs éléments.
<!-- hx-sync -->
<form hx-post="/store">
<input id="title" name="title" type="text"
hx-post="/validate"
hx-trigger="change"
hx-sync="closest form:abort">
<button type="submit">Submit</button>
</form>
Paramètre : paramètres inclus dans la requête. Par défaut, la valeur de l'élément ou les inputs
d'un formulaire sont inclus.
Pour filtrer ou ajouter des paramètres à une requête :
<!-- hx-include, hx-params, hx-vals -->
<div hx-get="/example" hx-params="*"></div>
<div hx-get="/example" hx-vals='{"myVal": "My Value"}'>
<button hx-post="/send" hx-include="[name='email']">Send</button>
<input name="email" type="email"/>
Pour sélectionner les éléments d'une réponse qui remplaceront la cible :
<!-- hx-select, hx-select-oob -->
<button hx-get="/info" hx-select="#info-details" hx-swap="outerHTML">
Get Info!
</button>
<div id="alert"></div>
<button hx-get="/info"
hx-select="#info-details"
hx-swap="outerHTML"
hx-select-oob="#alert">
Get Info!
</button>
Confirmation : fenêtre de confirmation
<!-- hx-confirm -->
<button hx-delete="/account" hx-confirm="Are you sure you wish to delete your account?">
Delete My Account
</button>
Héritage : les attributs héritables s'appliquent à l'élément qui les porte ainsi qu'aux éléments enfants.
<div hx-confirm="Are you sure?">
<button hx-delete="/account">Delete</button>
<button hx-put="/account">Update</button>
<button hx-get="/" hx-confirm="unset">Cancel</button>
</div>
Boost : conversion des liens et formulaires HTML classiques en requêtes AJAX ayant pour cible le body
de la page.
<!-- hx-boost -->
<div hx-boost="true">
<a href="/link">Link</a>
</div>
Historique : ajoute une URL dans l'historique du navigateur.
<!-- hx-push-url -->
<a hx-get="/link" hx-push-url="true">Link</a>
WebSockets et Server Sent Events :
<!-- hx-ws, hx-sse -->
<div hx-ws="connect:wss:/chatroom">
<div id="chat_room">
...
</div>
<form hx-ws="send:submit">
<input name="chat_message">
</form>
</div>
<body hx-sse="connect:/news_updates">
<div hx-get="/news" hx-trigger="sse:new_news"></div>
</body>
Astuce : les fragments de gabarit permettent de regrouper des gabarits partiels dans un seul fichier : django-template-partials, jinja2-fragments.
Alpine.js
Alpine.js est un framework Javascript minimaliste utilisable en HTML.
Installation
L'installation consiste à copier la librairie et l'inclure dans le head
de la page avec une balise script
:
<script defer src="/path/to/alpine.min.js"></script>
Se référer à la documentation.
Utilisation
Alpine.js est un framework réactif dans le sens où lorsqu'une donnée est modifiée, tout ce qui dépend de cette donnée "réagit" automatiquement à ce changement.
Exemple de déclaration d'une donnée, déclenchement d'un évènement et réaction au changement de la donnée :
<div x-data="{ open: false }">
<button @click="open = true">Expand</button>
<span x-show="open">
Content...
</span>
</div>
Principaux attributs :
x-data Declare data for a block of HTML
x-bind Set HTML attributes on an element
x-on Listen events on an element
x-text Set the text content of an element
x-html Set the inner HTML of an element
x-model Synchronize data with an input element
x-show Toggle the visibility of an element
x-for Repeat a block of HTML based on a data set
x-if Conditionally add/remove a block of HTML
x-init Run code when an element is initialized
x-effect Execute a script each time one of its dependancies change
x-ref Reference an element by key
Principales propriétés :
$store Access a global store registered using Alpine.store()
$el Reference the current DOM element
$dispatch Dispatch a custom event from the current element
$watch Watch data and run the provided callback anytime it changes
$refs Access an element by key referenced by x-ref
Principales méthodes :
Alpine.data() Define a data object used by x-data
Alpine.store() Define a global data accessed using $store
Pagefind
Pagefind est une bibliothèque de recherche statique optimisée pour utiliser le moins de bande passante possible. Contrairement à Lunr.js qui construit un index de recherche unique, Pagefind le divise en fragments ordonnés. D'autres outils de recherche en texte intégral (full-text search) sont listés dans la documentation de Hugo.
Construction de l'index de recherche
Télecharger et exécuter le binaire précompilé pagefind :
./pagefind --site public
Affichage des résultats de recherche
Insérer les scripts CSS et Javascript suivants dans une page :
<link href="/pagefind/pagefind-ui.css" rel="stylesheet">
<script src="/pagefind/pagefind-ui.js"></script>
<div id="search"></div>
<script>
window.addEventListener('DOMContentLoaded', (event) => {
new PagefindUI({ element: "#search", showSubResults: true });
});
</script>
Prévoir une méthode alternative dans la cas où le Javascript serait désactivé (recherche DuckDuckGo par exemple) :
<noscript>
<form action="https://duckduckgo.com/" target="_blank" rel="noopener">
<input type="hidden" name="sites" value="nora.nckm.eu">
<input type="hidden" name="ko" value="-2">
<input type="hidden" name="k1" value="-1">
<input type="hidden" name="kz" value="-1">
<input type="hidden" name="km" value="m">
<input type="hidden" name="kae" value="d">
<input type="hidden" name="k7" value="#212121">
<label for="q">DuckDuckGo</label>
<input type="search" name="q" id="q">
<input type="submit" value="Rechercher">
</form>
</noscript>