← Projets


Emely.es — Site web en Rust

Ce site est lui-même un projet : un site web multilingue et auto-hébergé, construit de zéro en Rust. La production tourne sur un VPS Linux ; le homelab héberge le dépôt Forgejo et le runner CI qui compile et déploie à chaque push sur main.

Pourquoi Rust ?

Rust a été choisi délibérément. Le runtime n'a pas de ramasse-miettes, pas d'allocation mémoire cachée, et démarre en quelques millisecondes. Pour un site de contenu comme celui-ci, cela signifie que le serveur est systématiquement rapide — sous charge ou à froid — et que le binaire est suffisamment petit pour résider confortablement dans un conteneur Podman.

Le framework est Axum, une bibliothèque HTTP ergonomique construite sur le runtime asynchrone de Tokio.

Comment ça fonctionne

Tout le contenu vit dans des fichiers Markdown bruts, organisés par langue et section. Au démarrage, le serveur lit chaque fichier exactement une fois :

  1. Parsing du frontmatter — auteur et date sont extraits du bloc --- en tête de chaque fichier, avant que le contenu ne soit transmis au moteur Markdown.
  2. Markdown → HTML — pulldown-cmark convertit le corps en HTML. Le résultat est stocké en mémoire.
  3. Pré-construction des listes et de la navigation — le HTML des listes de chaque section et les éléments déroulants de l'en-tête sont assemblés une fois et mis en cache, de sorte que le rendu d'une page se réduit à quelques substitutions de chaînes.
  4. Chargement des traductions — les fichiers translations.txt par langue (paires clé = valeur) sont parsés en table de correspondance.

Les templates HTML sont embarqués directement dans le binaire à la compilation via la macro include_str!() de Rust. Il n'y a pas de moteur de templates — juste une convention {{ PLACEHOLDER }} et des appels à .replace() au moment de la requête. Le résultat est un serveur avec zéro I/O disque par requête.

Architecture

flowchart TD
    classDef visitor fill:#f9f9f9,stroke:#333,stroke-width:2px;
    classDef infra fill:#e1f5fe,stroke:#01579b,stroke-width:2px;
    classDef runtime fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
    classDef source fill:#fff3e0,stroke:#ef6c00,stroke-width:2px;
    classDef pipeline fill:#ede7f6,stroke:#4527a0,stroke-width:2px;
    classDef cicd fill:#fce4ec,stroke:#880e4f,stroke-width:2px;

    Visitor([Navigateur])

    subgraph VPS [VPS — Linux]
        NGINX["NGINX · terminaison TLS"]
        Static["Fichiers statiques · CSS · favicon"]

        subgraph Container [Conteneur Podman]
            Router["Routeur Axum\n/{lang}/blog · /services · /projects · /cv"]
            Handler["Gestionnaire de requête"]
            Templates["Templates HTML\ninclude_str! — compilés dans le binaire"]
            AppState["Arc<AppState> — cache partagé en lecture seule"]
        end
    end

    subgraph Homelab [Homelab — origine CI/CD]
        Forgejo["Forgejo\nGit + CI"]
        ForgejoRunner["Forgejo runner"]
    end

    subgraph Pipeline [Pipeline de démarrage — exécuté une fois au lancement]
        MDFiles[("Fichiers Markdown\nresources/en · es · fr")]
        FrontMatter["Parser de frontmatter\nauteur · date"]
        Cmark["pulldown-cmark\nMarkdown → HTML"]
        Prebuilt["HTML pré-construit\nlistes · nav dropdowns · traductions"]
    end

    Visitor -->|HTTPS| NGINX
    NGINX --> Router
    Router --> Handler
    Handler --> Templates
    Handler --> AppState
    Templates -->|Réponse HTML| Visitor
    Static -->|ServeDir| Visitor

    Forgejo -->|push déclenche| ForgejoRunner
    ForgejoRunner -->|build + déploiement| VPS

    MDFiles --> FrontMatter
    FrontMatter --> Cmark
    Cmark --> Prebuilt
    Prebuilt -->|chargé dans| AppState

    class Visitor visitor;
    class NGINX infra;
    class Router,Handler,Templates,AppState runtime;
    class Static source;
    class MDFiles,FrontMatter,Cmark,Prebuilt pipeline;
    class Forgejo,ForgejoRunner cicd;

Support multilingue

Le site sert l'anglais, l'espagnol et le français depuis le même binaire. Le préfixe d'URL (/en/, /es/, /fr/) détermine la langue. Le contenu est organisé en répertoires par langue, et les templates portent des marqueurs {{ T_CLÉ }} résolus au moment du rendu. Les slugs en anglais servent de clé canonique — les autres langues reviennent à la version anglaise si une traduction est absente.

Infrastructure

Déployé en conteneur Podman sur un VPS Linux. NGINX termine le TLS et reverse-proxifie le trafic vers le conteneur. Le pipeline de livraison vit sur le homelab : le code source est hébergé sur une instance Forgejo auto-gérée, et un runner Forgejo prend en charge chaque push, compile le binaire et déploie le conteneur mis à jour sur le VPS. Pas de cloud. Pas de service CI managé. Pas de base de données.

Déploiement

Chaque changement passe par un environnement de staging avant d'atteindre la production. Le même runner Forgejo qui gère les déploiements en production gère aussi le staging — un conteneur séparé tournant la même image, accessible en interne pour validation.

Promouvoir en production consiste en un git merge vers main. Le runner détecte le push, compile le binaire de release et redéploie le conteneur de production sur le VPS.

sequenceDiagram
    actor Dev as Développeur
    participant Repo as Forgejo (homelab)
    participant Runner as Forgejo runner
    participant Staging as Conteneur staging
    participant Prod as VPS — Production

    Dev->>Repo: push sur branche staging
    Repo->>Runner: déclencher pipeline CI
    Runner->>Runner: cargo build --release
    Runner->>Staging: déployer conteneur
    Runner-->>Dev: staging prêt pour validation

    Note over Dev,Staging: valider sur staging

    Dev->>Repo: git merge staging → main
    Repo->>Runner: déclencher pipeline CI (main)
    Runner->>Runner: cargo build --release
    Runner->>Prod: déployer conteneur Podman
    Runner-->>Dev: production mise à jour

Stack

Rust · Axum · pulldown-cmark · tower-http · Forgejo CI · Mermaid.js