← Proyectos


Emely.es — Web en Rust

Este sitio es en sí mismo un proyecto: una web multilingüe y autoalojada, construida desde cero en Rust. La producción corre en un VPS Linux; el homelab aloja el repositorio Forgejo y el runner de CI que compila y despliega en cada push a main.

¿Por qué Rust?

Rust fue elegido de forma deliberada. El runtime no tiene recolector de basura, ni asignación de memoria oculta, y arranca en milisegundos. Para un sitio de contenido como este, eso significa que el servidor es consistentemente rápido — bajo carga o en frío — y el binario es lo suficientemente pequeño para vivir cómodamente dentro de un contenedor Podman.

El framework es Axum, una librería HTTP ergonómica construida sobre el runtime asíncrono de Tokio.

Cómo funciona

Todo el contenido vive en archivos Markdown planos, organizados por idioma y sección. En el arranque, el servidor lee cada archivo exactamente una vez:

  1. Parseo de frontmatter — autor y fecha se extraen del bloque --- al principio de cada archivo, antes de pasar el contenido al renderizador Markdown.
  2. Markdown → HTML — pulldown-cmark convierte el cuerpo a HTML. El resultado se almacena en memoria.
  3. Pre-construcción de listas y navegación — el HTML de las listas de cada sección y los elementos desplegables de la cabecera se ensamblan una vez y se cachean, de modo que renderizar una página es simplemente unas pocas sustituciones de cadenas.
  4. Carga de traducciones — los archivos translations.txt por idioma (pares clave = valor) se parsean en una tabla de consulta.

Las plantillas HTML se embeben directamente en el binario en tiempo de compilación con la macro include_str!() de Rust. No hay motor de plantillas — solo una convención {{ PLACEHOLDER }} y llamadas a .replace() en el momento de la petición. El resultado es un servidor con cero I/O de archivos en tiempo de ejecución por petición.

Arquitectura

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

    subgraph VPS [VPS — Linux]
        NGINX["NGINX · terminación TLS"]
        Static["Archivos estáticos · CSS · favicon"]

        subgraph Container [Contenedor Podman]
            Router["Router Axum\n/{lang}/blog · /services · /projects · /cv"]
            Handler["Handler de petición"]
            Templates["Plantillas HTML\ninclude_str! — compiladas en el binario"]
            AppState["Arc<AppState> — caché compartida de solo lectura"]
        end
    end

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

    subgraph Pipeline [Pipeline de arranque — se ejecuta una vez al iniciar]
        MDFiles[("Archivos Markdown\nresources/en · es · fr")]
        FrontMatter["Parser de frontmatter\nautor · fecha"]
        Cmark["pulldown-cmark\nMarkdown → HTML"]
        Prebuilt["HTML pre-construido\nlistas · nav dropdowns · traducciones"]
    end

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

    Forgejo -->|push dispara| ForgejoRunner
    ForgejoRunner -->|compilar + desplegar| VPS

    MDFiles --> FrontMatter
    FrontMatter --> Cmark
    Cmark --> Prebuilt
    Prebuilt -->|cargado en| 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;

Soporte multilingüe

El sitio sirve inglés, español y francés desde el mismo binario. El prefijo de URL (/en/, /es/, /fr/) determina el idioma. El contenido está organizado en directorios por idioma y las plantillas llevan marcadores {{ T_CLAVE }} que se resuelven en el momento de renderizar. Los slugs en inglés actúan como clave canónica — los demás idiomas recurren a la versión inglesa si falta una traducción.

Infraestructura

Desplegado como contenedor Podman en un VPS Linux. NGINX termina el TLS y hace reverse proxy hacia el contenedor. El pipeline de entrega vive en el homelab: el código fuente está alojado en una instancia Forgejo autogestionada, y un runner de Forgejo recoge cada push, compila el binario y despliega el contenedor actualizado en el VPS. Sin proveedor cloud. Sin servicio de CI gestionado. Sin base de datos.

Flujo de despliegue

Cada cambio pasa por un entorno de staging antes de llegar a producción. El mismo runner de Forgejo que gestiona los despliegues en producción también gestiona el staging — un contenedor separado con la misma imagen, accesible internamente para revisión.

Promocionar a producción es un git merge a main. El runner detecta el push, compila el binario de release y redespliega el contenedor de producción en el VPS.

sequenceDiagram
    actor Dev as Desarrollador
    participant Repo as Forgejo (homelab)
    participant Runner as Forgejo runner
    participant Staging as Contenedor staging
    participant Prod as VPS — Producción

    Dev->>Repo: push a rama staging
    Repo->>Runner: disparar pipeline CI
    Runner->>Runner: cargo build --release
    Runner->>Staging: desplegar contenedor
    Runner-->>Dev: staging listo para revisión

    Note over Dev,Staging: validar en staging

    Dev->>Repo: git merge staging → main
    Repo->>Runner: disparar pipeline CI (main)
    Runner->>Runner: cargo build --release
    Runner->>Prod: desplegar contenedor Podman
    Runner-->>Dev: producción actualizada

Stack

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