Rusty Full Stack

El blog para los amantes de Rust, Ionic y Vuejs

Jaime Blandón
Jaime Blandón Desarrollador de software desde 2009, entusiasta de Rust, Vuejs y Ionic!. Fundador de este blog, espero que las publicaciones te sean de utilidad y si tienes comentarios para mejorar, son bienvenidos.

Organizar templates HTML con actix-web y tera

Hola!

Esta es una continuación del artículo anterior sobre desarrollo web con Rust y actix-web 😎 Por si aún no lo has leído, puedes hacerlo desde el enlace:

Desarrollo web con Rust - State y Templates HTML.

En dicho artículo, creamos la página inicial de un blog utilizando Rust, actix-web y tera para el manejo de templates HTML. La página se miraba similar a la de la imagen:

Sin embargo, para crear esa página inicial, utilizamos únicamente un template HTML al cual llamamos "index.html" y sobre ese template colocamos todo nuestro código HTML y cargamos todos nuestros recursos estáticos, de igual forma, solamente creamos la página inicial que carga todos nuestros posts, pero no hicimos ninguna página para mostrar el contenido un post individual por lo que en este artículo nos encargaremos de crearla.

¿Qué significa organizar templates HTML y por qué es importante?

Cuando trabajamos en proyectos de desarrollo web ya sea con Rust o en cualquier otro lenguaje de programación, a medida vamos agregando funcionalidades, paquetes, etc. es común que nuestro código vaya creciendo e incluso también es común que escribamos funciones, módulos o librerías para poderlas utilizar sin necesidad de volver a escribir el mismo código una y otra vez. Este crecimiento, de código, también nos obliga a darle algún tipo de mantenimiento o buscar entre todas estas líneas de código, aquellos bloques que nos puedan interesar, por lo que es mucho más sencillo realizar todas esas tareas si organizamos nuestro código en carpetas y archivos estructurados de forma tal que nos facilite su lectura, a todo eso es lo que llamamos organización de código.

En Rust, podemos organizar nuestro código utilizando módulos y librerías, en el blog tenemos una serie sobre organización de código, por si te gustaría saber más al respecto, te dejo el primer artículo de dicha serie sobre cómo poder organizar código con Rust:

Organizar código en Rust.

Podrías entonces ahora preguntarte cómo se puede organizar código HTML sin utilizar ningún frameworks javascript como React, vue, Angular, etc. y aunque a lo mejor el primer pensamiento sea "pues simplemente creo un archivo html para cada página que necesite", esto a su vez tiene algunas desventajas, por ejemplo, es probable que muchas secciones de nuestra app web sea común durante toda la experiencia de usuario, como por ejemplo un menú en la parte superior de nuestra app, el pie de página, hojas de estilos, etc. Si realizamos la implementación de crear un archivo html por cada página, entonces también es bastante probable que repitamos una y otra vez los mismos bloques de código, cuando podríamos intentar reutilizarlos; sin embargo, es probable que sepas que HTML no tiene el concepto de funciones ya que no es un lenguaje de programación sino más bien un lenguaje de marcado o descriptivo.

Entonces, si no es posible crear funciones en HTML y no vamos a utilizar un framework javascript para la creación de componentes reutilizables, ¿cómo podemos organizar nuestro código HTML 🤔?

Organizar HTML con Actix-web y Tera.

Si has seguido la serie de desarrollo web con Rust y actix-web, te podrás dar cuente que estamos utilizar el motor de templates HTML llamado tera, el cual nos permite cargar un template HTML y pasarle un contexto, el cual luego es reemplazado en el template por el contenido que queremos mostrar.

Tera, provee un mecanismo llamado Base Template, el cual nos permite poder crear una plantilla e indicar una serie de "bloques" que luego podremos reemplazar en nuestro Child Template, el cual nos facilita reutilizar código HTML que se repite una y otra vez en nuestro proyecto.

Es importante destacar que podemos construir tantos Base Templates o Child Templates como requiramos, siempre y cuando nos facilite la organización de nuestro código. Si vienes del mundo de Django o jinja2 estos conceptos te sonará bastante familiares 😉

Como siempre, es mucho más fácil comprender estos conceptos haciendo algún ejemplo.

Nota: no es necesario haber leído el artículo anterior para poder realizar nuestro ejemplo, sin embargo, si te recomiendo echarle un vistazo para que puedas familiarizarte ya que nuestro código inicial será  justamente el ejemplo que se creó.

Si realizaste el ejemplo del artículo anterior, puedes utilizar el mismo código como punto de inicio, por si no lo has leído o no tienes el código, puede utilizar este como punto de partida:

Código.

También puedes ver el código completo del ejemplo desde el repositorio de github.

Bloques HTML con Tera.

Tera nos permite definir dentro de un Base Template, aquellas secciones que queremos reutilizar para que luego el motor únicamente reemplace lo que se encuentre dentro de un bloque (block) por lo que queremos mostrar en pantalla a nuestros usuarios.

Ahora con nuestro código clonado, vamos a iniciar definiendo las secciones que creamos que podemos reutilizar constantemente en nuestro proyecto.

Analizado nuestro código dentro de templates/index.html, podríamos volver a utilizar las siguientes secciones una y otra vez, en especial si queremos agregar una nueva página para mostrar los detalles de un post específico:

  • El código base <html>
  • El "banner"

Para nuestro ejemplo con las secciones anteriores serían suficientes,  pero, si tu app web tiene otras secciones que se repiten en toda la experiencia de usuario como por ejemplo algún menú, pie de página, etc. tu puedes organizar para cada sección que creas conveniente.

Ahora que hemos decidido las secciones, antes de modificar nuestros templates HTML, vamos a crear un método que nos permita obtener un post en particular de nuestro "listado de posts", puesto que la mayor parte de la explicación del listado de posts se encuentra en el artículo de State y Templates HTML con actix-web, no entraremos en mucho detalle sobre el módulo de posts. Dentro de src/posts.rs puedes reemplazar el contenido con el siguiente código:

        
        
            // src/posts.rs

use serde::Serialize;

#[derive(Serialize)]
pub struct Post {
    pub id: String,
    pub titulo: String,
    pub description_corta: String,
    pub autor: String,
    pub avatar: String,
    pub imagen_encabezado: String,
    pub contenido: String,
    pub publicado: bool,
    pub fecha_publicacion: String,
}

pub fn obtener_todos_los_posts() -> Vec<Post> {
    vec![
        Post {
            id: String::from("mi-post"),
            titulo: String::from("Mi Post"),
            description_corta: String::from("Este es mi <b>primer post</b>"),
            autor: String::from("Rusty Full Stack"),
            avatar: String::from("/static/img/avatar.jpg"),
            imagen_encabezado: String::from("/static/img/post1.jpg"),
            contenido: String::from(
                r#"<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p> Proin sit amet dictum ipsum, eu volutpat nulla. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam porttitor felis non fringilla fringilla. Sed vitae ultrices eros. Suspendisse quis nibh vel sapien volutpat venenatis sit amet ut eros. Aliquam sit amet tellus non tortor dapibus scelerisque. Etiam finibus hendrerit nibh, nec vestibulum nisl porta et. Nullam imperdiet est sit amet scelerisque tempus. Ut nibh ipsum, fringilla vitae nisi ut, gravida blandit erat. Praesent venenatis ante eu venenatis pretium. Morbi mollis arcu sapien, id condimentum eros porta ac.
    Aliquam feugiat eros vitae nisi ultrices, dictum tincidunt risus volutpat. Ut eleifend turpis eget fringilla congue. Aenean in tortor lobortis, vehicula eros ac, fringilla arcu. Fusce volutpat justo ut turpis convallis facilisis. Cras et blandit purus. Nunc convallis consequat libero. Donec mattis tortor sit amet vestibulum gravida. Donec nec egestas quam, quis pharetra est. Praesent id ante sed mauris auctor malesuada. Donec lobortis ultricies feugiat. Nullam vestibulum feugiat porta."#,
            ),
            publicado: true,
            fecha_publicacion: String::from("2024-02-09"),
        },
        Post {
            id: String::from("creado-templates-html-con-actix-web"),
            titulo: String::from("Creado Templates HTML con actix-web"),
            description_corta: String::from("Aprendiendo actix-web con <a href='https://rustyfullstack.com'>rustyfullstack.com!</a>"),
            autor: String::from("Rusty Full Stack"),
            imagen_encabezado: String::from("/static/img/post2.jpg"),
            avatar: String::from("/static/img/avatar.jpg"),
            contenido: String::from(
                r#"<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin sit amet dictum ipsum, eu volutpat nulla.</p> Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam porttitor felis non fringilla fringilla. Sed vitae ultrices eros. Suspendisse quis nibh vel sapien volutpat venenatis sit amet ut eros. Aliquam sit amet tellus non tortor dapibus scelerisque. Etiam finibus hendrerit nibh, nec vestibulum nisl porta et. Nullam imperdiet est sit amet scelerisque tempus. Ut nibh ipsum, fringilla vitae nisi ut, gravida blandit erat. Praesent venenatis ante eu venenatis pretium. Morbi mollis arcu sapien, id condimentum eros porta ac.
    Aliquam feugiat eros vitae nisi ultrices, dictum tincidunt risus volutpat. Ut eleifend turpis eget fringilla congue. Aenean in tortor lobortis, vehicula eros ac, fringilla arcu. Fusce volutpat justo ut turpis convallis facilisis. Cras et blandit purus. Nunc convallis consequat libero. Donec mattis tortor sit amet vestibulum gravida. Donec nec egestas quam, quis pharetra est. Praesent id ante sed mauris auctor malesuada. Donec lobortis ultricies feugiat. Nullam vestibulum feugiat porta."#,
            ),
            publicado: true,
            fecha_publicacion: String::from("2024-02-09"),
        },
    ]
}

pub fn obtener_post(id: &str) -> Option<Post> {
    let posts: Vec<Post> = obtener_todos_los_posts();
    if let Some(post_encontrado) = posts.into_iter().find(|post| post.id == id) {
        Some(post_encontrado)
    } else {
        None
    }
}

        
        
    

El código anterior agrega un nuevo campo "id" a nuestra estructura Post, el campo id será utilizado para realizar la búsqueda de un post determinado, también cambia el valor de "publicado" de los posts del listado a true.

La parte más importante de nuestro código anterior es esta función:

        
        
            
pub fn obtener_post(id: &str) -> Option<Post> {
    let posts: Vec<Post> = obtener_todos_los_posts();
    if let Some(post_encontrado) = posts.into_iter().find(|post| post.id == id) {
        Some(post_encontrado)
    } else {
        None
    }
}
        
        
    

Esta función se encarga de realizar una búsqueda dentro del listado de todos los posts, el cual es devuelto por la función obtener_todos_los_posts, una vez obtenido el listado total, entonces se utiliza u "iterador" utilizado into_iter (por si no sabes que son los iteradores puedes leer el artículo Iteradores en Rust Con Ejemplos), hay que notar que se utiliza into_iter en lugar de solamente iter, para que el iterador tome posesión de cada item y de esta forma no tener que hacer un clone de cada String, porque iter solo nos brinda la referencia y esta referencia no se podría devolver porque la seguridad de memoria nos indicaría un error al momento de compilar el proyecto. Otra cosa importante es que encuentra el post, entonces lo devuelve:

Some(post_encontrado)

Si no lo encuentra, entonces retorna un None. Debido a que un post se puede encontrar o no, por eso la función está definida para regresar:

Option<Post>

Ahora que ya tenemos nuestra función, podemos iniciar a modificar el código de nuestros templates y poder organizarlos mejor 💪.

Vamos ahora, dentro de la carpeta templates, a crear un nuevo directorio al que llamaremos "blog" y dentro de "blog" crearemos nos archivos, uno llamado "posts.html" y el otro llamado "post.html", debería verse de esta forma:

Como puedes ver, comenzamos a organizar archivos en carpetas y archivos más descriptivos. Ahora, vamos a utilizar nuestro archivo templates/index.html como nuestro Base Template. El archivo puede llamarse de cualquier manera, pero es casi una convención no escrita el utilizar el nombre "base.html", por lo que vamos a renombrarlo y ahora se debería de ver de esta forma:

Con nuestros archivos organizamos de mejor manera, ahora vamos a crear nuestros bloques dentro de nuestro Base Template:

        
        
            <!-- templates/base.html -->
<!doctype html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{% block titulo %} {% endblock %}</title>
    <link rel="stylesheet" href="/static/css/bootstrap.min.css" />
  </head>
  <body>
    <!-- Banner -->
    <section class="jumbotron text-center">
      <div class="container">
        <h1 class="jumbotron-heading">Mi Blog</h1>
        <p class="lead text-muted">Bienvenido a mi blog!</p>
      </div>
    </section>
    <!-- Fin Banner -->
    <div class="bg-light">
      <div class="container">{% block contenido %}{% endblock %}</div>
    </div>
    <script src="/static/js/bootstrap.min.js"></script>
  </body>
</html>

        
        
    

Ahora nuestro template ya tiene definidos los bloques que luego serán reemplazados:

        
        
            
<title>{% block titulo %} {% endblock %}</title>

<div class="container">{% block contenido %}{% endblock %}</div>

        
        
    

Todo lo demás que NO SEA UN BLOQUE, será reutilizado una y otra vez por lo que ya no debemos reescribirlo, y todo dentro de los bloques será dinámico.

Es importante notar la nomenclatura de un bloque, el cual se define así:

{%block <nombre>%}

por ejemplo:

{% block titulo %}

indica que un Child component puede tener un bloque llamado "titulo" (o no, los bloques no son obligatorios) y se reemplazará dentro del Base Template, el titulo que se coloque en el Child Component. Para verlo más claro, coloca el siguiente código en templates/blog/posts.html el cual funcionará como un Child Component.

        
        
            <!-- templates/blog/posts.html -->

{% extends "base.html" %} 

{% block titulo %} Mi Blog {% endblock %} 

{% block contenido %} 
    {% for post in posts %} 
        {% if post.publicado %}
            <div class="card mb-3">
              <img
                src="{{ post.imagen_encabezado}}"
                class="card-img-top"
                alt="..."
                style="height: 40vh"
              />
              <div class="card-body">
                <h5 class="card-title">{{ post.titulo }}</h5>
                <div class="container-fluid p-0">
                  <img
                    src="{{ post.avatar }}"
                    alt=""
                    class="rounded-circle"
                    style="width: 3em"
                  />
                  <span><i>{{ post.autor }}</i></span>
                </div>
                <p class="card-text">{{ post.description_corta | safe }}</p>
                <p class="card-text">
                  <small class="text-body-secondary"
                    >Publicado: {{ post.fecha_publicacion }}</small
                  >
                </p>
              </div>
            </div>
        {% endif %} 
    {% endfor %} 
{% endblock %}

        
        
    

Como puedes ver, lo primero que en el child component se define, es el Base Component de referencia, para ello se utiliza la línea:

{% extends "base.html" %}

Ahora dentro de nuestro child template, tenemos este bloque:

{% block titulo %} Mi Blog {% endblock %} 

El cual le indica a tera, que dentro de base.html, reemplace el contenido dentro del bloque "titulo" por "Mi Blog", es decir esto:

        
        
            <!-- templates/base.html -->
<titulo> {% block titulo %} </titulo>
        
        
    

Se convertirá en:

        
        
            
<titulo> Mi Blog </titulo>
        
        
    

El bloque "contenido" dentro de:

        
        
            <!-- templates/base.html -->
<div class="container">{% block contenido %}{% endblock %}</div>

        
        
    

se convertirá en exactamente el mismo HTML que teníamos anteriormente donde recorremos todos los posts publicados, pero mejor organizado.

Ahora para poder probarlo debemos modificar de donde se lee el template HTML en nuestro src/main.rs reemplaza por este código:

        
        
            // src/main.rs

mod archivos_estaticos;
mod posts;

use actix_web::{get, web, App, HttpResponse, HttpServer};
use archivos_estaticos::leer_archivo_estatico;
use posts::{obtener_todos_los_posts, Post};
use tera::{Context, Tera};

// Es importante notar que para acceder al State que se guardo
// con .app_data, se coloca como parametro en la definicion del metodo
// el web::Data<T> donde T es el tipo de dato que pasamos en .app_data
// Lo genial es que si no necesitamos utilizar Tera, simplemente podemos
// quitarlo de la definicion, es decir, no es obligatorio poner en las
// definicionnes de las funciones todo lo que se ecuentre en el state
#[get("/")]
async fn index(tera: web::Data<Tera>) -> HttpResponse {
    // Ahora creamos el Context para pasarle valores a nuestro template HTML
    // es importante notar que le agregamos un "mut" porque vamos a modificar
    // sus valores
    let mut context = Context::new();
    // Ahora agregamos el {{nombre}} que espera nuestro template
    // para  ello el key debe coincidir con lo especificado en el template
    // en este caso el valor que se pasa a "posts" son todos los Posts que seran serializados por
    // Serde
    let posts: Vec<Post> = obtener_todos_los_posts();
    context.insert("posts", &posts);
    // Ahora hacemos el render de nuestro template, pasando los valores
    // del contexto para ser reemplazados al momento del render.
    let respuesta_html = tera.render("blog/posts.html", &context).unwrap();
    HttpResponse::Ok().body(respuesta_html)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        // Creamos una configuracion de Tera, en la cual se especificamos
        // que todos los  templates se encuetran en la carpeta "templates"
        // en la raiz del proyecto y que ademas dicha carpeta puede contener sub carpetas
        let tera: Tera = Tera::new("templates/**/*").unwrap();
        App::new()
            .service(leer_archivo_estatico)
            // Aca agregamos al "State" nuestra configuracio de tera
            // para agregar al State, utilizamos el metodo .app_data
            // al cual le pasamos de parametro un web::Data para poder
            // manejar de forma segura la memoria junto con la configuracion
            // de tera
            .app_data(web::Data::new(tera))
            .service(index)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

        
        
    

Lo único que ha cambiado es esta línea:

        
        
            
let respuesta_html = tera.render("index.html", &context).unwrap();

        
        
    

Pasa a llamar al Child Component:

        
        
            
let respuesta_html = tera.render("blog/posts.html", &context).unwrap();

        
        
    

Si ahora corremos nuestro programa con:

cargo rust

Verás un warning que por el momento vamos a ignorar y que arreglaremos en un momento.

Y si ingresamos a:

http://localhost:8080

Vemos que nuestros posts siguen funcionando, pero está vez con nuestro código mejor organizado 🥳.

Veamos ahora la ventaja de utilizar templates creando la página de detalle de los blogs, pero sin necesidad de reescribir muchas cosas 🥹

Como ya tenemos nuestro base template, solamente necesitaremos crear un Child template para mostrar el contenido del blog. Dentro de templates/blog/post.html podemos colocar el siguiente código:

        
        
            <!-- templates/blog/post.html -->
{% extends "base.html" %}

{% block titulo %} 
    {{ post.titulo }} 
{% endblock %} 

{% block contenido %}

<div class="card mb-3">
  <img
    src="{{ post.imagen_encabezado}}"
    class="card-img-top"
    alt="..."
    style="height: 40vh"
  />
  <div class="card-body">
    <h5 class="card-title">{{ post.titulo }}</h5>
    <div class="container-fluid p-0">
      <img
        src="{{ post.avatar }}"
        alt=""
        class="rounded-circle"
        style="width: 3em"
      />
      <span><i>{{ post.autor }}</i></span>
    </div>
    <p class="card-text">{{ post.contenido | safe }}</p>
    <p class="card-text">
      <small class="text-body-secondary"
        >Publicado: {{ post.fecha_publicacion }}</small
      >
    </p>
  </div>
</div>

{% endblock %}

        
        
    

Como puedes ver, el código anterior es únicamente la parte del contenido del blog, pero, no hay que olvidar que debemos agregar una nueva ruta en nuestro archivo src/main.rs, el cual, deberá de tener un parámetro de url que en este c aso será el id del post (en otro artículo hablaremos más a detalle sobre el tipo de parámetros manejados por actix-web). Una vez recibido el id, entonces buscaremos un post que coincida con ese id y lo mostraremos en nuestro detalle del post.

En src/main.rs puedes reemplazar su contenido con el siguiente código:

        
        
            // src/main.rs
mod archivos_estaticos;
mod posts;

use actix_web::{get, web, App, HttpResponse, HttpServer};
use archivos_estaticos::leer_archivo_estatico;
use posts::{obtener_post, obtener_todos_los_posts, Post};
use tera::{Context, Tera};

// Es importante notar que para acceder al State que se guardo
// con .app_data, se coloca como parametro en la definicion del metodo
// el web::Data<T> donde T es el tipo de dato que pasamos en .app_data
// Lo genial es que si no necesitamos utilizar Tera, simplemente podemos
// quitarlo de la definicion, es decir, no es obligatorio poner en las
// definicionnes de las funciones todo lo que se ecuentre en el state
#[get("/")]
async fn index(tera: web::Data<Tera>) -> HttpResponse {
    // Ahora creamos el Context para pasarle valores a nuestro template HTML
    // es importante notar que le agregamos un "mut" porque vamos a modificar
    // sus valores
    let mut context = Context::new();
    // Ahora agregamos el {{nombre}} que espera nuestro template
    // para  ello el key debe coincidir con lo especificado en el template
    // en este caso el valor que se pasa a "posts" son todos los Posts que seran serializados por
    // Serde
    let posts: Vec<Post> = obtener_todos_los_posts();
    context.insert("posts", &posts);
    // Ahora hacemos el render de nuestro template, pasando los valores
    // del contexto para ser reemplazados al momento del render.
    let respuesta_html = tera.render("blog/posts.html", &context).unwrap();
    HttpResponse::Ok().body(respuesta_html)
}

// Esta es la forma de pasar un parametro por la ruta del url, en la definicion
// de la funcion, es importante poder definir el tipo de los parametros
#[get("/{id}")]
async fn post(tera: web::Data<Tera>, path: web::Path<String>) -> HttpResponse {
    let mut context = Context::new();

    //  el parametro "path" contiene todos los parametros pasados por url, en
    //  este especifico caso solo es uno de tipo String, si hubierann mas, entonces
    //  habria que especificar cada tipo o crear una estructura que contenga esos
    //  parametros, puedes aprender mas de parametros de ruta en la documentacion:
    //  https://actix.rs/docs/extractors#path

    let id = path.into_inner(); // obteniendo el id
    if let Some(post) = obtener_post(&id) {
        // Si encuentra un post verificamos si tiene publicado = true
        if post.publicado {
            context.insert("post", &post);
            let respuesta_html = tera.render("blog/post.html", &context).unwrap();
            HttpResponse::Ok().body(respuesta_html)
        } else {
            // Si no esta publicado todavia, entonces regresaremos un error 404,
            // a nivel de experiencia de usuario no se vera de la mejor forma
            // pero aprenderemos a menajar errores de una mejor forma en otro articulo
            HttpResponse::NotFound().body("Post no encontrado")
        }
    } else {
        // En este caso si o encuentra el post, retornaremos un error 404 (Not Found)
        // Al ver el error, se mostrara un contenido bastante "feo", aprenderemos
        // en otro articulo una mejor forma de manejar los errores
        HttpResponse::NotFound().body("Post no encotrado")
    }
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        // Creamos una configuracion de Tera, en la cual se especificamos
        // que todos los  templates se encuetran en la carpeta "templates"
        // en la raiz del proyecto y que ademas dicha carpeta puede contener sub carpetas
        let tera: Tera = Tera::new("templates/**/*").unwrap();
        App::new()
            .service(leer_archivo_estatico)
            // Aca agregamos al "State" nuestra configuracio de tera
            // para agregar al State, utilizamos el metodo .app_data
            // al cual le pasamos de parametro un web::Data para poder
            // manejar de forma segura la memoria junto con la configuracion
            // de tera
            .app_data(web::Data::new(tera))
            .service(index)
            .service(post)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

        
        
    

El código anterior agrega una función mediante método GET para leer el contenido de un post pasándole un id de post como parámetro de ruta:

        
        
            
// Esta es la forma de pasar un parametro por la ruta del url, en la definicion
// de la funcion, es importante poder definir el tipo de los parametros
#[get("/{id}")]
async fn post(tera: web::Data<Tera>, path: web::Path<String>) -> HttpResponse {
    let mut context = Context::new();

    //  el parametro "path" contiene todos los parametros pasados por url, en
    //  este especifico caso solo es uno de tipo String, si hubierann mas, entonces
    //  habria que especificar cada tipo o crear una estructura que contenga esos
    //  parametros, puedes aprender mas de parametros de ruta en la documentacion:
    //  https://actix.rs/docs/extractors#path

    let id = path.into_inner(); // obteniendo el id
    if let Some(post) = obtener_post(&id) {
        // Si encuentra un post verificamos si tiene publicado = true
        if post.publicado {
            context.insert("post", &post);
            let respuesta_html = tera.render("blog/post.html", &context).unwrap();
            HttpResponse::Ok().body(respuesta_html)
        } else {
            // Si no esta publicado todavia, entonces regresaremos un error 404,
            // a nivel de experiencia de usuario no se vera de la mejor forma
            // pero aprenderemos a menajar errores de una mejor forma en otro articulo
            HttpResponse::NotFound().body("Post no encontrado")
        }
    } else {
        // En este caso si o encuentra el post, retornaremos un error 404 (Not Found)
        // Al ver el error, se mostrara un contenido bastante "feo", aprenderemos
        // en otro articulo una mejor forma de manejar los errores
        HttpResponse::NotFound().body("Post no encotrado")
    }
}
        
        
    

El código anterior posee algunas validaciones que en caso no cumplirse retornaría un error 404 (No Encontrado), como por ejemplo si un usuario cambia manualmente el id en la url por alguno que o exista, sin embargo, NO RECOMIENDO manejar los errores de esa forma, en este ejemplo lo he colocado así porque no hemos visto como manejar errores con actix-web, eso lo aprenderemos en el siguiente artículo 😉

Lo único que nos queda hacer es simplemente agregar un enlace en nuestro listado de posts para pasar en la url el id del post, esto lo lograremos con un link de esta forma:

        
        
            
<a href="/{{ post.id }}> {{ post.titulo }} </a>
        
        
    

para poner ese link, reemplaza el contenido del archivo templates/blog/posts.html por el siguiente código:

        
        
            <!--  templates/blog/posts.html -->

{% extends "base.html" %} 

{% block titulo %} Mi Blog {% endblock %} 

{% block contenido %} 
    {% for post in posts %} 
        {% if post.publicado %}
            <div class="card mb-3">
              <img
                src="{{ post.imagen_encabezado}}"
                class="card-img-top"
                alt="..."
                style="height: 40vh"
              />
              <div class="card-body">
                <h5 class="card-title">
                    <a href="/{{post.id}}">
                        {{ post.titulo }}
                    </a>
                </h5>
                <div class="container-fluid p-0">
                  <img
                    src="{{ post.avatar }}"
                    alt=""
                    class="rounded-circle"
                    style="width: 3em"
                  />
                  <span><i>{{ post.autor }}</i></span>
                </div>
                <p class="card-text">{{ post.description_corta | safe }}</p>
                <p class="card-text">
                  <small class="text-body-secondary"
                    >Publicado: {{ post.fecha_publicacion }}</small
                  >
                </p>
              </div>
            </div>
        {% endif %} 
    {% endfor %} 
{% endblock %}

        
        
    

Ahora únicamente queda probarlo 😃. En la terminal ejecuta el comando:

cargo run

Ahora ingresa a http://localhost:8080

Verás que ahora los títulos son enlaces:

Si das click a cualquiera de los enlaces, te llevará a la página donde se muestra el contenido del post y el banner sigue ahí porque lo reutilizamos desde el Base Template!!

Si observas en url del post, verás que se está agregando el id del post:

Nota: Si pones un id que no exista, verás un error como el siguiente (tiene un status code 404), el manejo de errores lo ajustaremos en el siguiente artículo.

Felicidades! ahora tenemos también nuestra página con los detalles de un post y estamos dejando mejor organizados nuestros archivos 🥳

¿Qué tal si ahora queremos agregar otra sección que queremos en cada página?

Por ejemplo si queremos agregarle un footer el cual debe de estar disponible tanto en el listado de posts como en el detalle de un post no necesitamos agregarlo en cada Child Template, basta agregarlo en el Base template una sola vez. Por ejemplo si modificamos templates/base.html con el siguiente código:

        
        
            <!-- templates/base.html -->
<!doctype html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{% block titulo %} {% endblock %}</title>
    <link rel="stylesheet" href="/static/css/bootstrap.min.css" />
  </head>
  <body>
    <!-- Banner -->
    <section class="jumbotron text-center">
      <div class="container">
        <h1 class="jumbotron-heading">Mi Blog</h1>
        <p class="lead text-muted">Bienvenido a mi blog!</p>
      </div>
    </section>
    <!-- Fin Banner -->
    <div class="bg-light">
      <div class="container">{% block contenido %}{% endblock %}</div>
    </div>

    <!-- footer -->
    <footer class="d-flex flex-wrap justify-content-between align-items-center py-3 my-4 border-top">
        <div class="col-md-4 d-flex align-items-center">
          <span class="mb-3 mb-md-0 text-muted">Mi super footer</span>
        </div>
    </footer>

    <script src="/static/js/bootstrap.min.js"></script>
  </body>
</html>

        
        
    

Si ahora volvemos a ejecutar nuestra app web, veremos que se ha agregado un footer tanto en la página principal, como en el detalle de un post, agregando el footer solamente en un lugar!!

cargo run

Footer en la página principal

Footer en el detalle del post

Hemos aprendido sobre las ventajas de organizar nuestro código html utilizando Rust, actix-web y tera 😎

Recuerda que puedes ver el código completo desde el repositorio de github.

Si esta publicación te ha sido de utilidad, compártela con tus amigos y en las redes sociales. No te olvides de seguirme en X (twitter) ni de suscribirte al canal de youtube.

Si esta información te ha sido de utilidad y te gustaría apoyar a la creación de más artículos y contenidos puedes convertirte en mecenas desde patreon te estaré sumamente agradecido 🦀

println!("Hasta la próxima!");


 Utilizamos cookies propias y de terceros para mejorar tu experiencia, mostrar publicidad y análisis de navegación, puedes encontrar el detalle en nuestra Política de Cookies