← Projects


Emely.es — Website in Rust

This site is itself a project: a self-hosted, multilingual website built from scratch in Rust. Production runs on a Linux VPS; the homelab hosts the Forgejo repository and the CI runner that builds and deploys on every push to main.

Why Rust?

Rust was chosen deliberately. The runtime has no garbage collector, no hidden memory allocation, and starts in milliseconds. For a content site like this, that means the server is consistently fast — under load or cold — and the binary is small enough to sit comfortably inside a Podman container.

The framework is Axum, an ergonomic HTTP library built on Tokio's async runtime.

How it works

All content lives in plain Markdown files, organised by language and section. At startup, the server reads every file exactly once:

  1. Frontmatter parsing — author and date are extracted from the --- block at the top of each file before the content is passed to the Markdown renderer.
  2. Markdown → HTML — pulldown-cmark converts the body to HTML. The result is stored in memory.
  3. Pre-building lists and nav — list HTML for each section and the header dropdown items are assembled once and cached, so rendering a page is just a few string substitutions.
  4. Translations loaded — per-language translations.txt files (key = value pairs) are parsed into a lookup table.

HTML templates are embedded directly into the binary at compile time with Rust's include_str!() macro. There is no template engine — just a {{ PLACEHOLDER }} convention and .replace() calls at request time. The result is a server with zero runtime file I/O per request.

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([Browser])

    subgraph VPS [VPS — Linux]
        NGINX["NGINX · TLS termination"]
        Static["Static files · CSS · favicon"]

        subgraph Container [Podman container]
            Router["Axum router\n/{lang}/blog · /services · /projects · /cv"]
            Handler["Request handler"]
            Templates["HTML templates\ninclude_str! — compiled into binary"]
            AppState["Arc<AppState> — shared read-only cache"]
        end
    end

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

    subgraph Pipeline [Startup pipeline — runs once at boot]
        MDFiles[("Markdown files\nresources/en · es · fr")]
        FrontMatter["Frontmatter parser\nauthor · date"]
        Cmark["pulldown-cmark\nMarkdown → HTML"]
        Prebuilt["Pre-built HTML\nlist pages · nav dropdowns · translations"]
    end

    Visitor -->|HTTPS| NGINX
    NGINX --> Router
    Router --> Handler
    Handler --> Templates
    Handler --> AppState
    Templates -->|HTML response| Visitor
    Static -->|ServeDir| Visitor

    Forgejo -->|push triggers| ForgejoRunner
    ForgejoRunner -->|build + deploy| VPS

    MDFiles --> FrontMatter
    FrontMatter --> Cmark
    Cmark --> Prebuilt
    Prebuilt -->|loaded into| 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;

Multilingual support

The site serves English, Spanish, and French from the same binary. URL prefix (/en/, /es/, /fr/) determines the language. Content is organised into per-language directories and templates carry {{ T_KEY }} placeholders resolved at render time. English slugs act as the canonical key — other languages fall back to the English version if a translation is missing.

Infrastructure

Deployed as a Podman container on a Linux VPS. NGINX terminates TLS and reverse-proxies traffic to the container. The delivery pipeline lives on the homelab: source code is hosted on a self-managed Forgejo instance, and a Forgejo runner picks up every push, builds the binary, and deploys the updated container to the VPS. No cloud provider. No managed CI service. No database.

Staging

Every change goes through a staging environment before reaching production. The same Forgejo runner that handles production deployments manages staging too — a separate container running the same image, reachable internally for review.

Promoting to production is a git merge into main. The runner detects the push to main, builds the release binary, and redeploys the production container on the VPS.

sequenceDiagram
    actor Dev as Developer
    participant Repo as Forgejo (homelab)
    participant Runner as Forgejo runner
    participant Staging as Staging container
    participant Prod as VPS — Production

    Dev->>Repo: push to staging branch
    Repo->>Runner: trigger CI pipeline
    Runner->>Runner: cargo build --release
    Runner->>Staging: deploy container
    Runner-->>Dev: staging ready for review

    Note over Dev,Staging: validate on staging

    Dev->>Repo: git merge staging → main
    Repo->>Runner: trigger CI pipeline (main)
    Runner->>Runner: cargo build --release
    Runner->>Prod: deploy Podman container
    Runner-->>Dev: production updated

Stack

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