Comprendre les décisions UI : SSR, HTMX et DaisyUI

Ce que vous allez comprendre

  • Analyser pourquoi gitrust utilise le rendu côté serveur (SSR) plutôt qu'un framework JavaScript front-end.
  • Évaluer comment HTMX ajoute de l'interactivité sans introduire de build front-end.
  • Décrire les contraintes qui ont orienté le choix de DaisyUI comme bibliothèque de composants.

Le problème concret

Une forge Git moderne doit proposer des interfaces réactives : autocomplétion, chargement partiel, mises à jour en temps réel (logs CI, progression d'import). Les approches habituelles imposent soit un SPA React/Vue avec son pipeline de build, soit un back-end qui sert une API JSON distincte de ses vues.

Pour une équipe de 3 personnes maintenant un outil en Rust, les coûts de ces approches sont disproportionnés : - Un SPA impose une deuxième codebase (TypeScript), deux langages de test, deux pipelines CI. - Un back-end JSON-only perd les avantages du rendu serveur (SEO, accessibilité, temps de chargement initial).

L'analogie

SSR + HTMX fonctionne comme un serveur HTML des années 2000 avec superpouvoirs : le serveur produit du HTML complet que le navigateur affiche directement (SSR classique), mais HTMX permet au navigateur de remplacer des fragments de la page sans recharger le tout. C'est la différence entre remplacer une page entière de livre et coller un post-it sur un paragraphe.

DaisyUI est la couche de présentation : elle apporte des composants CSS cohérents (boutons, badges, modales) sans JavaScript, uniquement via des classes Tailwind.

Le modèle

Architecture UI de gitrust

flowchart LR
    Browser[Navigateur]
    Axum[Axum SSR\nAskama templates]
    HTMX[htmx.js\n~14 KB gzippé]
    DaisyUI[DaisyUI + Tailwind\nbundle CSS < 300 KB]
    AlpineJS[Alpine.js\n~15 KB gzippé]

    Browser -->|"GET /alice/myrepo"| Axum
    Axum -->|"HTML complet"| Browser
    Browser -->|"hx-get=/fragment"| Axum
    Axum -->|"Fragment HTML"| Browser

    Browser --- HTMX
    Browser --- DaisyUI
    Browser --- AlpineJS

Aucun CDN externe. Tous les assets sont servis localement depuis static/. Le navigateur ne fait jamais de requête vers un domaine tiers.

Rendu côté serveur — Askama

Gitrust utilise Askama, un moteur de templates Rust compilé. Les templates sont vérifiés au moment de la compilation : une clé manquante ou un type incompatible est une erreur de build, pas une erreur à l'exécution.

// crates/gitrust-web/src/templates.rs
#[derive(Template)]
#[template(path = "repository/show.html")]
pub struct RepositoryShowTemplate {
    pub repo: RepositoryView,
    pub branches: Vec<BranchSummary>,
    pub current_user: Option<UserView>,
    pub csrf_token: String,
}

Le handler construit la struct, passe-la au template, et Askama sérialise le HTML :

async fn repo_show(/* ... */) -> impl IntoResponse {
    let tmpl = RepositoryShowTemplate {
        repo: repo_view,
        branches,
        current_user: user.map(|u| u.into()),
        csrf_token: generate_csrf(&session),
    };
    Html(tmpl.render().unwrap())
}

Avantage clé : le compilateur Rust garantit que chaque champ exposé au template existe et a le bon type. Impossible d'afficher un champ None non géré.

HTMX — interactivité sans JavaScript custom

HTMX étend HTML avec des attributs hx-*. Le navigateur envoie des requêtes HTTP et remplace des fragments DOM avec la réponse.

Autocomplétion des subject labels

<!-- templates/issues/partials/subject_tags.html -->
<input
  type="text"
  name="tag_query"
  hx-get="/{{ owner }}/{{ repo }}/labels/search"
  hx-trigger="input changed delay:300ms, keyup[key=='Tab']"
  hx-target="#tag-suggestions"
  hx-swap="innerHTML"
  placeholder="Ajouter un tag..."
/>
<ul id="tag-suggestions"></ul>

Le handler correspondant retourne un fragment HTML, pas du JSON :

async fn search_subject_labels(
    Path((owner, repo)): Path<(String, String)>,
    Query(params): Query<HashMap<String, String>>,
    State(db): State<DatabaseConnection>,
) -> impl IntoResponse {
    let query = params.get("tag_query").map(|s| s.as_str()).unwrap_or("");
    let labels = LabelService::search_subject_labels(&db, &repo_id, query).await?;
    let tmpl = LabelSuggestionsTemplate { labels };
    Html(tmpl.render().unwrap())
}

Logs CI en temps réel (SSE)

Pour les logs de pipeline qui s'affichent en continu, gitrust utilise les Server-Sent Events exposés par Axum, consommés via hx-ext="sse" :

<div
  hx-ext="sse"
  sse-connect="/{{ owner }}/{{ repo }}/ci/{{ pipeline_id }}/stream"
  sse-swap="message"
  hx-swap="beforeend"
  id="log-output"
>
</div>

Le serveur envoie des événements data: <ligne HTML>\n\n. HTMX les append dans #log-output sans aucun JavaScript custom.

DaisyUI — composants CSS, zéro JavaScript

DaisyUI est une bibliothèque de composants construite sur Tailwind CSS. Ses composants (bouton, badge, dropdown, modal) s'activent uniquement via des classes CSS et des attributs HTML natifs (<details>, <input type="checkbox">).

<!-- Badge classification (fond plein) -->
<span class="badge badge-primary">{{ label.name }}</span>

<!-- Badge subject (contour) -->
<span class="badge badge-outline" style="border-color: {{ label.color }}">
  {{ label.name }}
</span>

<!-- Dropdown natif DaisyUI -->
<details class="dropdown">
  <summary class="btn btn-sm">Assignees</summary>
  <ul class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow">
    {% for collaborator in collaborators %}
    <li><a hx-post="..." hx-vals='{"user_id": "{{ collaborator.id }}"}'>
      {{ collaborator.username }}
    </a></li>
    {% endfor %}
  </ul>
</details>

Aucun composant DaisyUI ne nécessite d'initialisation JavaScript. Les modales utilisent <input type="checkbox" id="modal-toggle"> avec CSS :checked pour afficher/masquer.

Alpine.js — état local léger

Pour les rares cas nécessitant de l'état côté client (toggle d'un panneau, validation inline), gitrust utilise Alpine.js (~15 KB) plutôt que React ou Vue.

<!-- Toggle d'un panneau de settings -->
<div x-data="{ open: false }">
  <button @click="open = !open">Paramètres avancés</button>
  <div x-show="open" x-transition>
    <!-- contenu du panneau -->
  </div>
</div>

Alpine.js est réservé aux comportements purement visuels sans appel réseau. Dès qu'un appel serveur est nécessaire, c'est HTMX qui prend le relais.

Budget bundle et contraintes

Asset Taille gzippée
htmx.min.js ~14 KB
alpine.min.js ~15 KB
gitrust.css (DaisyUI + Tailwind purged) ~280 KB
Total < 310 KB

Le CSS est généré lors du build via npx tailwindcss --input ... --output ... --minify. Le résultat est commité dans static/css/gitrust.css pour éviter une dépendance Node.js en production. La reconstruction est déclenchée uniquement quand les templates changent (gate QA : make css-rebuild).

Quand utiliser chaque outil

flowchart TD
    Q1{Interaction nécessite\nun appel serveur ?}
    Q1 -->|Oui| Q2{Contenu retourné ?}
    Q1 -->|Non| Alpine[Alpine.js\nx-data, x-show, @click]
    Q2 -->|Fragment HTML| HTMX[HTMX\nhx-get / hx-post]
    Q2 -->|Flux continu| SSE[HTMX + SSE\nhx-ext='sse']
    Q2 -->|JSON uniquement| Fetch[fetch() natif\n(rare, éviter)]

Alternatives et compromis

Pourquoi pas React/Vue/Svelte ? Ces frameworks nécessitent un pipeline de build (Vite, Webpack), une API JSON séparée, et des compétences TypeScript en plus de Rust. Pour une forge auto-hébergée ciblant 3-20 développeurs, ce coût n'est pas justifié.

Pourquoi pas Inertia.js (SSR + SPA hybride) ? Inertia demande un adaptateur côté serveur. Il n'existe pas d'adaptateur Rust/Axum stable. De plus, Inertia reste JavaScript-first.

Pourquoi pas Turbo (Hotwire) ? Turbo est excellent mais son modèle Frame/Stream est plus complexe que les attributs HTMX pour des équipes qui démarrent. HTMX est plus proche de HTML pur.

Compromis assumé : HTMX ne peut pas gérer des interactions très complexes (éditeur de code en ligne, diagrammes interactifs). Si gitrust devait ajouter un éditeur Monaco, un composant React isolé serait envisageable — mais ce n'est pas une décision prise aujourd'hui.

Vérifier votre compréhension

  1. Un développeur propose d'ajouter une feature de « preview Markdown en temps réel » dans l'éditeur de description d'issue. Il suggère d'installer React juste pour ce composant. Quelles sont les alternatives possibles avec la stack existante ? Évaluez leurs compromis.

  2. La page /alice/myrepo/issues charge 100 issues et est lente. Un développeur propose de migrer vers une SPA React avec pagination côté client. Quelle solution HTMX permettrait d'obtenir le même résultat sans changer de stack ?

Pour aller plus loin