diff --git a/src/html.rs b/src/html.rs
index 14e72b9..14a89ed 100644
--- a/src/html.rs
+++ b/src/html.rs
@@ -2,3 +2,77 @@
mod maud;
pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, Render, DOCTYPE};
+
+mod assets;
+pub use assets::favicon::Favicon;
+pub use assets::javascript::JavaScript;
+pub use assets::stylesheet::{StyleSheet, TargetMedia};
+pub(crate) use assets::Assets;
+
+mod context;
+pub use context::{Context, ErrorParam};
+
+use crate::AutoDefault;
+
+/// Prepara contenido HTML para su conversión a [`Markup`].
+///
+/// Este tipo encapsula distintos orígenes de contenido HTML (texto plano, HTML escapado o marcado
+/// ya procesado) para renderizar de forma homogénea en plantillas sin interferir con el uso
+/// estándar de [`Markup`].
+///
+/// # Ejemplo
+///
+/// ```rust
+/// use pagetop::prelude::*;
+///
+/// let fragment = PrepareMarkup::Text(String::from("Hola mundo"));
+/// assert_eq!(fragment.render().into_string(), "Hola <b>mundo</b>");
+///
+/// let raw_html = PrepareMarkup::Escaped(String::from("negrita"));
+/// assert_eq!(raw_html.render().into_string(), "negrita");
+///
+/// let prepared = PrepareMarkup::With(html! {
+/// h2 { "Título de ejemplo" }
+/// p { "Este es un párrafo con contenido dinámico." }
+/// });
+/// assert_eq!(
+/// prepared.render().into_string(),
+/// "
Título de ejemplo
Este es un párrafo con contenido dinámico.
"
+/// );
+/// ```
+#[derive(AutoDefault)]
+pub enum PrepareMarkup {
+ /// No se genera contenido HTML (devuelve `html! {}`).
+ #[default]
+ None,
+ /// Texto estático que se escapará automáticamente para no ser interpretado como HTML.
+ Text(String),
+ /// Contenido sin escapado adicional, útil para HTML generado externamente.
+ Escaped(String),
+ /// Fragmento HTML ya preparado como [`Markup`], listo para insertarse directamente.
+ With(Markup),
+}
+
+impl PrepareMarkup {
+ /// Devuelve `true` si el contenido está vacío y no generará HTML al renderizar.
+ pub fn is_empty(&self) -> bool {
+ match self {
+ PrepareMarkup::None => true,
+ PrepareMarkup::Text(text) => text.is_empty(),
+ PrepareMarkup::Escaped(string) => string.is_empty(),
+ PrepareMarkup::With(markup) => markup.is_empty(),
+ }
+ }
+}
+
+impl Render for PrepareMarkup {
+ /// Integra el renderizado fácilmente en la macro [`html!`].
+ fn render(&self) -> Markup {
+ match self {
+ PrepareMarkup::None => html! {},
+ PrepareMarkup::Text(text) => html! { (text) },
+ PrepareMarkup::Escaped(string) => html! { (PreEscaped(string)) },
+ PrepareMarkup::With(markup) => html! { (markup) },
+ }
+ }
+}
diff --git a/src/html/assets.rs b/src/html/assets.rs
new file mode 100644
index 0000000..894b7e8
--- /dev/null
+++ b/src/html/assets.rs
@@ -0,0 +1,63 @@
+pub mod favicon;
+pub mod javascript;
+pub mod stylesheet;
+
+use crate::html::{html, Markup, Render};
+use crate::{AutoDefault, Weight};
+
+pub trait AssetsTrait: Render {
+ // Devuelve el nombre del recurso, utilizado como clave única.
+ fn name(&self) -> &str;
+
+ // Devuelve el peso del recurso, durante el renderizado se procesan de menor a mayor peso.
+ fn weight(&self) -> Weight;
+}
+
+#[derive(AutoDefault)]
+pub(crate) struct Assets(Vec);
+
+impl Assets {
+ pub fn new() -> Self {
+ Assets::(Vec::::new())
+ }
+
+ pub fn add(&mut self, asset: T) -> bool {
+ match self.0.iter().position(|x| x.name() == asset.name()) {
+ Some(index) => {
+ if self.0[index].weight() > asset.weight() {
+ self.0.remove(index);
+ self.0.push(asset);
+ true
+ } else {
+ false
+ }
+ }
+ _ => {
+ self.0.push(asset);
+ true
+ }
+ }
+ }
+
+ pub fn remove(&mut self, name: impl AsRef) -> bool {
+ if let Some(index) = self.0.iter().position(|x| x.name() == name.as_ref()) {
+ self.0.remove(index);
+ true
+ } else {
+ false
+ }
+ }
+}
+
+impl Render for Assets {
+ fn render(&self) -> Markup {
+ let mut assets = self.0.iter().collect::>();
+ assets.sort_by_key(|a| a.weight());
+
+ html! {
+ @for a in assets {
+ (a.render())
+ }
+ }
+ }
+}
diff --git a/src/html/assets/favicon.rs b/src/html/assets/favicon.rs
new file mode 100644
index 0000000..97a44e8
--- /dev/null
+++ b/src/html/assets/favicon.rs
@@ -0,0 +1,165 @@
+use crate::html::{html, Markup, Render};
+use crate::AutoDefault;
+
+/// Un **Favicon** es un recurso gráfico que usa el navegador como icono asociado al sitio.
+///
+/// Es universalmente aceptado para mostrar el icono del sitio (`.ico`, `.png`, `.svg`, …) en
+/// pestañas, marcadores o accesos directos.
+///
+/// Este tipo permite construir de forma fluida las distintas variantes de un *favicon*, ya sea un
+/// icono estándar, un icono Apple para la pantalla de inicio, o un icono para Safari con color.
+/// También puede aplicar colores al tema o configuraciones específicas para *tiles* de Windows.
+///
+/// > **Nota**
+/// > Los archivos de los iconos deben estar disponibles en el servidor web de la aplicación. Pueden
+/// > incluirse en el proyecto utilizando [`include_files`](crate::include_files) y servirse con
+/// > [`include_files_service`](crate::include_files_service).
+///
+/// # Ejemplo
+///
+/// ```rust
+/// use pagetop::prelude::*;
+///
+/// let favicon = Favicon::new()
+/// // Estándar de facto admitido por todos los navegadores.
+/// .with_icon("/icons/favicon.ico")
+///
+/// // Variante del favicon con tamaños explícitos: 32×32 y 16×16.
+/// .with_icon_for_sizes("/icons/favicon-32.png", "32x32")
+/// .with_icon_for_sizes("/icons/favicon-16.png", "16x16")
+///
+/// // Icono específico para accesos directos en la pantalla de inicio de iOS.
+/// .with_apple_touch_icon("/icons/apple-touch-icon.png", "180x180")
+///
+/// // Icono vectorial con color dinámico para pestañas ancladas en Safari.
+/// .with_mask_icon("/icons/safari-pinned-tab.svg", "#5bbad5")
+///
+/// // Personaliza la barra superior del navegador en Android Chrome (y soportado en otros).
+/// .with_theme_color("#ffffff")
+///
+/// // Personalizaciones específicas para "tiles" en Windows.
+/// .with_ms_tile_color("#da532c")
+/// .with_ms_tile_image("/icons/mstile-144x144.png");
+/// ```
+#[derive(AutoDefault)]
+pub struct Favicon(Vec);
+
+impl Favicon {
+ /// Crea un nuevo `Favicon` vacío.
+ ///
+ /// Equivalente a `Favicon::default()`. Se recomienda iniciar la secuencia de configuración
+ /// desde aquí.
+ pub fn new() -> Self {
+ Favicon::default()
+ }
+
+ // Favicon BUILDER *****************************************************************************
+
+ /// Le añade un icono genérico apuntando a `image`. El tipo MIME se infiere automáticamente a
+ /// partir de la extensión.
+ pub fn with_icon(self, image: impl Into) -> Self {
+ self.add_icon_item("icon", image.into(), None, None)
+ }
+
+ /// Le añade un icono genérico con atributo `sizes`, útil para indicar resoluciones específicas.
+ ///
+ /// El atributo `sizes` informa al navegador de las dimensiones de la imagen para que seleccione
+ /// el recurso más adecuado. Puede enumerar varias dimensiones separadas por espacios, p.ej.
+ /// `"16x16 32x32 48x48"` o usar `any` para iconos escalables (SVG).
+ ///
+ /// No es imprescindible, pero puede mejorar la selección del icono más adecuado.
+ pub fn with_icon_for_sizes(self, image: impl Into, sizes: impl Into) -> Self {
+ self.add_icon_item("icon", image.into(), Some(sizes.into()), None)
+ }
+
+ /// Le añade un *Apple Touch Icon*, usado por dispositivos iOS para las pantallas de inicio.
+ ///
+ /// Se recomienda indicar también el tamaño, p.ej. `"256x256"`.
+ pub fn with_apple_touch_icon(self, image: impl Into, sizes: impl Into) -> Self {
+ self.add_icon_item("apple-touch-icon", image.into(), Some(sizes.into()), None)
+ }
+
+ /// Le añade un icono para el navegador Safari, con un color dinámico.
+ ///
+ /// El atributo `color` lo usa Safari para colorear el trazado SVG cuando el icono se muestra en
+ /// modo *Pinned Tab*. Aunque Safari 12+ acepta *favicons normales*, este método garantiza
+ /// compatibilidad con versiones anteriores.
+ pub fn with_mask_icon(self, image: impl Into, color: impl Into) -> Self {
+ self.add_icon_item("mask-icon", image.into(), None, Some(color.into()))
+ }
+
+ /// Define el color del tema (``).
+ ///
+ /// Lo usan algunos navegadores para colorear la barra de direcciones o interfaces.
+ pub fn with_theme_color(mut self, color: impl Into) -> Self {
+ self.0.push(html! {
+ meta name="theme-color" content=(color.into());
+ });
+ self
+ }
+
+ /// Define el color del *tile* en Windows (``).
+ pub fn with_ms_tile_color(mut self, color: impl Into) -> Self {
+ self.0.push(html! {
+ meta name="msapplication-TileColor" content=(color.into());
+ });
+ self
+ }
+
+ /// Define la imagen del *tile* en Windows (``).
+ pub fn with_ms_tile_image(mut self, image: impl Into) -> Self {
+ self.0.push(html! {
+ meta name="msapplication-TileImage" content=(image.into());
+ });
+ self
+ }
+
+ // Función interna que centraliza la creación de las etiquetas ``.
+ //
+ // - `icon_rel`: indica el tipo de recurso (`"icon"`, `"apple-touch-icon"`, etc.).
+ // - `icon_source`: URL del recurso.
+ // - `icon_sizes`: tamaños opcionales.
+ // - `icon_color`: color opcional (solo relevante para `mask-icon`).
+ //
+ // También infiere automáticamente el tipo MIME (`type`) según la extensión del archivo.
+ fn add_icon_item(
+ mut self,
+ icon_rel: &str,
+ icon_source: String,
+ icon_sizes: Option,
+ icon_color: Option,
+ ) -> Self {
+ let icon_type = match icon_source.rfind('.') {
+ Some(i) => match icon_source[i..].to_owned().to_lowercase().as_str() {
+ ".avif" => Some("image/avif"),
+ ".gif" => Some("image/gif"),
+ ".ico" => Some("image/x-icon"),
+ ".jpg" | ".jpeg" => Some("image/jpeg"),
+ ".png" => Some("image/png"),
+ ".svg" => Some("image/svg+xml"),
+ ".webp" => Some("image/webp"),
+ _ => None,
+ },
+ _ => None,
+ };
+ self.0.push(html! {
+ link
+ rel=(icon_rel)
+ type=[(icon_type)]
+ sizes=[(icon_sizes)]
+ color=[(icon_color)]
+ href=(icon_source);
+ });
+ self
+ }
+}
+
+impl Render for Favicon {
+ fn render(&self) -> Markup {
+ html! {
+ @for item in &self.0 {
+ (item)
+ }
+ }
+ }
+}
diff --git a/src/html/assets/javascript.rs b/src/html/assets/javascript.rs
new file mode 100644
index 0000000..fb0a1b6
--- /dev/null
+++ b/src/html/assets/javascript.rs
@@ -0,0 +1,178 @@
+use crate::html::assets::AssetsTrait;
+use crate::html::{html, Markup, Render};
+use crate::{join, join_pair, AutoDefault, Weight};
+
+// Define el origen del recurso JavaScript y cómo debe cargarse en el navegador.
+//
+// Los distintos modos de carga permiten optimizar el rendimiento y controlar el comportamiento del
+// script.
+//
+// - [`From`] – Carga el script de forma estándar con la etiqueta ``. El parámetro `name` se usa como identificador interno del
+ /// *script*.
+ pub fn inline(name: impl Into, script: impl Into) -> Self {
+ JavaScript {
+ source: Source::Inline(name.into(), script.into()),
+ ..Default::default()
+ }
+ }
+
+ /// Crea un **script embebido** que se ejecuta automáticamente al terminar de cargarse el
+ /// documento HTML.
+ ///
+ /// El código se envuelve automáticamente en un `addEventListener('DOMContentLoaded', ...)`. El
+ /// parámetro `name` se usa como identificador interno del *script*.
+ pub fn on_load(name: impl Into, script: impl Into) -> Self {
+ JavaScript {
+ source: Source::OnLoad(name.into(), script.into()),
+ ..Default::default()
+ }
+ }
+
+ // JavaScript BUILDER **************************************************************************
+
+ /// Asocia una versión al recurso (usada para control de la caché del navegador).
+ ///
+ /// Si `version` está vacío, no se añade ningún parámetro a la URL.
+ pub fn with_version(mut self, version: impl Into) -> Self {
+ self.version = version.into();
+ self
+ }
+
+ /// Modifica el peso del recurso.
+ ///
+ /// Los recursos se renderizan de menor a mayor peso. Por defecto es `0`, que respeta el orden
+ /// de creación.
+ pub fn with_weight(mut self, value: Weight) -> Self {
+ self.weight = value;
+ self
+ }
+}
+
+impl AssetsTrait for JavaScript {
+ // Para *scripts* externos es la ruta; para *scripts* embebidos, un identificador.
+ fn name(&self) -> &str {
+ match &self.source {
+ Source::From(path) => path,
+ Source::Defer(path) => path,
+ Source::Async(path) => path,
+ Source::Inline(name, _) => name,
+ Source::OnLoad(name, _) => name,
+ }
+ }
+
+ fn weight(&self) -> Weight {
+ self.weight
+ }
+}
+
+impl Render for JavaScript {
+ fn render(&self) -> Markup {
+ match &self.source {
+ Source::From(path) => html! {
+ script src=(join_pair!(path, "?v=", self.version.as_str())) {};
+ },
+ Source::Defer(path) => html! {
+ script src=(join_pair!(path, "?v=", self.version.as_str())) defer {};
+ },
+ Source::Async(path) => html! {
+ script src=(join_pair!(path, "?v=", self.version.as_str())) async {};
+ },
+ Source::Inline(_, code) => html! {
+ script { (code) };
+ },
+ Source::OnLoad(_, code) => html! { (join!(
+ "document.addEventListener('DOMContentLoaded',function(){", code, "});"
+ )) },
+ }
+ }
+}
diff --git a/src/html/assets/stylesheet.rs b/src/html/assets/stylesheet.rs
new file mode 100644
index 0000000..7f64d18
--- /dev/null
+++ b/src/html/assets/stylesheet.rs
@@ -0,0 +1,174 @@
+use crate::html::assets::AssetsTrait;
+use crate::html::{html, Markup, PreEscaped, Render};
+use crate::{join_pair, AutoDefault, Weight};
+
+// Define el origen del recurso CSS y cómo se incluye en el documento.
+//
+// Los estilos pueden cargarse desde un archivo externo o estar embebidos directamente en una
+// etiqueta ``. El parámetro `name` se usa como identificador interno del
+ /// recurso.
+ pub fn inline(name: impl Into, styles: impl Into) -> Self {
+ StyleSheet {
+ source: Source::Inline(name.into(), styles.into()),
+ ..Default::default()
+ }
+ }
+
+ // StyleSheet BUILDER **************************************************************************
+
+ /// Asocia una versión al recurso (usada para control de la caché del navegador).
+ ///
+ /// Si `version` está vacío, no se añade ningún parámetro a la URL.
+ pub fn with_version(mut self, version: impl Into) -> Self {
+ self.version = version.into();
+ self
+ }
+
+ /// Modifica el peso del recurso.
+ ///
+ /// Los recursos se renderizan de menor a mayor peso. Por defecto es `0`, que respeta el orden
+ /// de creación.
+ pub fn with_weight(mut self, value: Weight) -> Self {
+ self.weight = value;
+ self
+ }
+
+ // StyleSheet EXTRAS ***************************************************************************
+
+ /// Especifica el medio donde se aplican los estilos.
+ ///
+ /// Según el argumento `media`:
+ ///
+ /// - `TargetMedia::Default` - Se aplica en todos los casos (medio por defecto).
+ /// - `TargetMedia::Print` - Se aplican cuando el documento se imprime.
+ /// - `TargetMedia::Screen` - Se aplican en pantallas.
+ /// - `TargetMedia::Speech` - Se aplican en dispositivos que convierten el texto a voz.
+ pub fn for_media(mut self, media: TargetMedia) -> Self {
+ self.media = media;
+ self
+ }
+}
+
+impl AssetsTrait for StyleSheet {
+ // Para hojas de estilos externas es la ruta; para las embebidas, un identificador.
+ fn name(&self) -> &str {
+ match &self.source {
+ Source::From(path) => path,
+ Source::Inline(name, _) => name,
+ }
+ }
+
+ fn weight(&self) -> Weight {
+ self.weight
+ }
+}
+
+impl Render for StyleSheet {
+ fn render(&self) -> Markup {
+ match &self.source {
+ Source::From(path) => html! {
+ link
+ rel="stylesheet"
+ href=(join_pair!(path, "?v=", self.version.as_str()))
+ media=[self.media.as_str_opt()];
+ },
+ Source::Inline(_, code) => html! {
+ style { (PreEscaped(code)) };
+ },
+ }
+ }
+}
diff --git a/src/html/context.rs b/src/html/context.rs
new file mode 100644
index 0000000..23e2be2
--- /dev/null
+++ b/src/html/context.rs
@@ -0,0 +1,216 @@
+use crate::core::TypeInfo;
+use crate::html::{html, Markup, Render};
+use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
+use crate::join;
+use crate::locale::{LanguageIdentifier, DEFAULT_LANGID};
+use crate::service::HttpRequest;
+
+use std::collections::HashMap;
+use std::error::Error;
+use std::str::FromStr;
+
+use std::fmt;
+
+/// Errores de lectura o conversión de parámetros almacenados en el contexto.
+#[derive(Debug)]
+pub enum ErrorParam {
+ /// El parámetro solicitado no existe.
+ NotFound,
+ /// El valor del parámetro no pudo convertirse al tipo requerido.
+ ParseError(String),
+}
+
+impl fmt::Display for ErrorParam {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ ErrorParam::NotFound => write!(f, "Parameter not found"),
+ ErrorParam::ParseError(e) => write!(f, "Parse error: {e}"),
+ }
+ }
+}
+
+impl Error for ErrorParam {}
+
+/// Representa el contexto asociado a un documento HTML.
+///
+/// Esta estructura se crea internamente para recoger información relativa al documento asociado,
+/// como la solicitud HTTP de origen, el idioma y los recursos *favicon* ([`Favicon`]), las hojas de
+/// estilo [`StyleSheet`], los *scripts* [`JavaScript`], o parámetros de contexto definidos en
+/// tiempo de ejecución.
+///
+/// # Ejemplo
+///
+/// ```rust
+/// use pagetop::prelude::*;
+///
+/// fn configure_context(mut ctx: Context) {
+/// // Establece el idioma del documento a español.
+/// ctx.set_langid(LangMatch::langid_or_default("es-ES"));
+///
+/// // Asigna un favicon.
+/// ctx.set_favicon(Some(Favicon::new().with_icon("/icons/favicon.ico")));
+///
+/// // Añade una hoja de estilo externa.
+/// ctx.add_stylesheet(StyleSheet::from("/css/style.css"));
+///
+/// // Añade un script JavaScript.
+/// ctx.add_javascript(JavaScript::defer("/js/main.js"));
+///
+/// // Añade un parámetro dinámico al contexto.
+/// ctx.set_param("usuario_id", 42);
+///
+/// // Recupera el parámetro y lo convierte a su tipo original.
+/// let id: i32 = ctx.get_param("usuario_id").unwrap();
+/// assert_eq!(id, 42);
+///
+/// // Genera un identificador único para un componente de tipo `Menu`.
+/// struct Menu;
+/// let unique_id = ctx.required_id::