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¶
-
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.
-
La page
/alice/myrepo/issuescharge 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 ?