✨ (html): Añade Preload para precarga de recursos
Nuevo tipo `Preload` (junto a `StyleSheet` y `JavaScript`) que emite `<link rel="preload">` con soporte para fuentes, estilos, scripts e imágenes (WebP y AVIF incluidos). Bootsier lo usa para precargar las fuentes Source Sans 3 y eliminar el FOUT en la carga inicial.
This commit is contained in:
parent
9b2d430d8b
commit
c7b680a7f7
6 changed files with 232 additions and 3 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
// Self-hosted Source Sans 3 (SIL OFL 1.1), served from /bootsier/fonts.
|
||||
// Required by AdminLTE 4, which declares it as the primary font family in $font-family-sans-serif.
|
||||
// Font URLs must match the Preload declarations in src/lib.rs; a mismatch causes double downloads.
|
||||
@font-face {
|
||||
font-family: "Source Sans 3";
|
||||
src: url("/bootsier/fonts/bootsier.font.woff2") format("woff2");
|
||||
|
|
|
|||
|
|
@ -155,7 +155,15 @@ impl Theme for Bootsier {
|
|||
}
|
||||
|
||||
fn before_render_page_body(&self, page: &mut Page) {
|
||||
page.alter_assets(AssetsOp::AddStyleSheet(
|
||||
// Las URLs de las fuentes deben coincidir exactamente con las declaradas en @font-face de
|
||||
// _bootsier-custom.scss; cualquier discrepancia hace que el navegador descargue dos veces.
|
||||
page.alter_assets(AssetsOp::AddPreload(
|
||||
Preload::font("/bootsier/fonts/bootsier.font.woff2").with_weight(-99),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddPreload(
|
||||
Preload::font("/bootsier/fonts/bootsier.font.italic.woff2").with_weight(-99),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddStyleSheet(
|
||||
StyleSheet::from("/bootsier/css/bootsier.min.css")
|
||||
.with_version(ADMINLTE_VERSION)
|
||||
.with_weight(-90),
|
||||
|
|
@ -168,7 +176,7 @@ impl Theme for Bootsier {
|
|||
.alter_assets(AssetsOp::AddJavaScript(
|
||||
JavaScript::defer("/bootsier/js/bootsier.extended.min.js")
|
||||
.with_version(ADMINLTE_VERSION)
|
||||
.with_weight(-89),
|
||||
.with_weight(-90),
|
||||
))
|
||||
.alter_child_in(
|
||||
&DefaultRegion::Footer,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use crate::core::TypeInfo;
|
|||
use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage};
|
||||
use crate::core::theme::all::DEFAULT_THEME;
|
||||
use crate::core::theme::{ChildrenInRegions, DefaultRegion, RegionRef, TemplateRef, ThemeRef};
|
||||
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
||||
use crate::html::{Assets, Favicon, JavaScript, Preload, StyleSheet};
|
||||
use crate::html::{Markup, Props, PropsOp, RoutePath, html};
|
||||
use crate::locale::L10n;
|
||||
use crate::locale::{LangId, LanguageIdentifier, RequestLocale};
|
||||
|
|
@ -23,6 +23,11 @@ pub enum AssetsOp {
|
|||
/// Define el *favicon* solo si no se ha establecido previamente.
|
||||
SetFaviconIfNone(Favicon),
|
||||
|
||||
/// Añade un recurso para precarga al documento.
|
||||
AddPreload(Preload),
|
||||
/// Elimina un recurso para precarga por su ruta.
|
||||
RemovePreload(&'static str),
|
||||
|
||||
/// Añade una hoja de estilos CSS al documento.
|
||||
AddStyleSheet(StyleSheet),
|
||||
/// Elimina una hoja de estilos por su ruta o identificador.
|
||||
|
|
@ -312,6 +317,7 @@ pub struct Context {
|
|||
theme : ThemeRef, // Referencia al tema usado para renderizar.
|
||||
template : TemplateRef, // Plantilla usada para renderizar.
|
||||
favicon : Option<Favicon>, // Favicon, si se ha definido.
|
||||
preloads : Assets<Preload>, // Recursos para precarga.
|
||||
stylesheets : Assets<StyleSheet>, // Hojas de estilo CSS.
|
||||
javascripts : Assets<JavaScript>, // Scripts JavaScript.
|
||||
body_props : Props, // Id, clases CSS y atributos del <body>.
|
||||
|
|
@ -343,6 +349,7 @@ impl Context {
|
|||
theme : *DEFAULT_THEME,
|
||||
template : DEFAULT_THEME.default_template(),
|
||||
favicon : None,
|
||||
preloads : Assets::<Preload>::new(),
|
||||
stylesheets: Assets::<StyleSheet>::new(),
|
||||
javascripts: Assets::<JavaScript>::new(),
|
||||
body_props : Props::default(),
|
||||
|
|
@ -370,6 +377,7 @@ impl Context {
|
|||
|
||||
// Extrae temporalmente los recursos.
|
||||
let favicon = mem_take(&mut self.favicon); // Deja valor por defecto (None) en self.
|
||||
let preloads = mem_take(&mut self.preloads); // Assets<Preload>::default() en self.
|
||||
let stylesheets = mem_take(&mut self.stylesheets); // Assets<StyleSheet>::default() en self.
|
||||
let javascripts = mem_take(&mut self.javascripts); // Assets<JavaScript>::default() en self.
|
||||
|
||||
|
|
@ -378,12 +386,15 @@ impl Context {
|
|||
@if let Some(fi) = &favicon {
|
||||
(fi.render(self))
|
||||
}
|
||||
// Primero los recursos para precarga para iniciar las descargas inmediatamente.
|
||||
(preloads.render(self))
|
||||
(stylesheets.render(self))
|
||||
(javascripts.render(self))
|
||||
};
|
||||
|
||||
// Restaura los campos tal y como estaban.
|
||||
self.favicon = favicon;
|
||||
self.preloads = preloads;
|
||||
self.stylesheets = stylesheets;
|
||||
self.javascripts = javascripts;
|
||||
|
||||
|
|
@ -549,6 +560,13 @@ impl Contextual for Context {
|
|||
self.favicon = Some(icon);
|
||||
}
|
||||
}
|
||||
// Preloads.
|
||||
AssetsOp::AddPreload(preload) => {
|
||||
self.preloads.add(preload);
|
||||
}
|
||||
AssetsOp::RemovePreload(path) => {
|
||||
self.preloads.remove(path);
|
||||
}
|
||||
// Stylesheets.
|
||||
AssetsOp::AddStyleSheet(css) => {
|
||||
self.stylesheets.add(css);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ pub use route::RoutePath;
|
|||
mod assets;
|
||||
pub use assets::favicon::Favicon;
|
||||
pub use assets::javascript::JavaScript;
|
||||
pub use assets::preload::Preload;
|
||||
pub use assets::stylesheet::{StyleSheet, TargetMedia};
|
||||
pub use assets::{Asset, Assets};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
pub mod favicon;
|
||||
pub mod javascript;
|
||||
pub mod preload;
|
||||
pub mod stylesheet;
|
||||
|
||||
use crate::core::component::Context;
|
||||
|
|
|
|||
200
src/html/assets/preload.rs
Normal file
200
src/html/assets/preload.rs
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
use crate::core::component::Context;
|
||||
use crate::html::assets::Asset;
|
||||
use crate::html::{Markup, html};
|
||||
use crate::{AutoDefault, CowStr, Weight, util};
|
||||
|
||||
// Tipo de recurso para el atributo `as` de un recurso para precargar.
|
||||
// Informa al navegador de la naturaleza del recurso para que pueda asignarle la prioridad correcta
|
||||
// y aplicar la política de caché adecuada.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) enum AsType {
|
||||
// Fuente web (`@font-face`). Implica `crossorigin` automático.
|
||||
#[default]
|
||||
Font,
|
||||
// Hoja de estilos CSS.
|
||||
Style,
|
||||
// Script JavaScript.
|
||||
Script,
|
||||
// Imagen.
|
||||
Image,
|
||||
}
|
||||
|
||||
impl AsType {
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
AsType::Font => "font",
|
||||
AsType::Style => "style",
|
||||
AsType::Script => "script",
|
||||
AsType::Image => "image",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Define un recurso **Preload** para precargar en el documento.
|
||||
///
|
||||
/// Indica al navegador que descargue el recurso con alta prioridad antes de que el analizador del
|
||||
/// HTML lo descubra de forma natural, reduciendo la latencia percibida.
|
||||
///
|
||||
/// > **Nota**
|
||||
/// > Los recursos deben estar disponibles en el servidor web de la aplicación. Pueden servirse
|
||||
/// > usando [`serve_static_files!`](crate::serve_static_files).
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// // Precarga una fuente web (crossorigin se activa automáticamente).
|
||||
/// let preload = Preload::font("/assets/fonts/inter.woff2").with_weight(-100);
|
||||
///
|
||||
/// // Precarga una imagen WebP crítica para la métrica "Largest Contentful Paint" (LCP).
|
||||
/// let preload = Preload::webp("/assets/img/hero.webp");
|
||||
/// ```
|
||||
#[derive(AutoDefault)]
|
||||
pub struct Preload {
|
||||
path: CowStr, // Ruta del recurso (href).
|
||||
as_type: AsType, // Tipo de recurso (atributo `as`).
|
||||
mime_type: CowStr, // Tipo MIME (atributo `type`), vacío si no se especifica.
|
||||
crossorigin: bool, // Activa el atributo `crossorigin`.
|
||||
version: CowStr, // Versión del recurso para la caché del navegador.
|
||||
weight: Weight, // Peso que determina el orden.
|
||||
}
|
||||
|
||||
impl Preload {
|
||||
/// Precarga una fuente web.
|
||||
///
|
||||
/// Equivale a `<link rel="preload" as="font" type="font/woff2" href="..." crossorigin>`.
|
||||
///
|
||||
/// El atributo `crossorigin` se activa siempre porque `@font-face` usa CORS internamente; sin
|
||||
/// él el navegador descargaría el recurso dos veces.
|
||||
pub fn font(path: impl Into<CowStr>) -> Self {
|
||||
Self {
|
||||
path: path.into(),
|
||||
as_type: AsType::Font,
|
||||
mime_type: "font/woff2".into(),
|
||||
crossorigin: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Precarga una hoja de estilos CSS.
|
||||
///
|
||||
/// Equivale a `<link rel="preload" as="style" href="...">`.
|
||||
pub fn style(path: impl Into<CowStr>) -> Self {
|
||||
Self {
|
||||
path: path.into(),
|
||||
as_type: AsType::Style,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Precarga un script JavaScript.
|
||||
///
|
||||
/// Equivale a `<link rel="preload" as="script" href="...">`.
|
||||
pub fn script(path: impl Into<CowStr>) -> Self {
|
||||
Self {
|
||||
path: path.into(),
|
||||
as_type: AsType::Script,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Precarga una imagen.
|
||||
///
|
||||
/// Equivale a `<link rel="preload" as="image" href="...">`. Adecuado para formatos con soporte
|
||||
/// universal (JPEG, PNG, GIF); para WebP o AVIF usar [`Self::webp()`] o [`Self::avif()`], que
|
||||
/// añaden el atributo `type` para que el navegador omita la descarga si no soporta el formato.
|
||||
pub fn image(path: impl Into<CowStr>) -> Self {
|
||||
Self {
|
||||
path: path.into(),
|
||||
as_type: AsType::Image,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Precarga una imagen en formato WebP.
|
||||
///
|
||||
/// Equivale a `<link rel="preload" as="image" type="image/webp" href="...">`.
|
||||
///
|
||||
/// El atributo `type` indica al navegador que omita la descarga si no soporta WebP.
|
||||
pub fn webp(path: impl Into<CowStr>) -> Self {
|
||||
Self {
|
||||
path: path.into(),
|
||||
as_type: AsType::Image,
|
||||
mime_type: "image/webp".into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Precarga una imagen en formato AVIF.
|
||||
///
|
||||
/// Equivale a `<link rel="preload" as="image" type="image/avif" href="...">`.
|
||||
///
|
||||
/// El atributo `type` indica al navegador que omita la descarga si no soporta AVIF.
|
||||
pub fn avif(path: impl Into<CowStr>) -> Self {
|
||||
Self {
|
||||
path: path.into(),
|
||||
as_type: AsType::Image,
|
||||
mime_type: "image/avif".into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// **< Preload 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<CowStr>) -> 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
|
||||
}
|
||||
|
||||
/// Activa el atributo `crossorigin`.
|
||||
///
|
||||
/// Necesario para recursos servidos desde otro origen. Para fuentes (`as="font"`) se activa
|
||||
/// automáticamente.
|
||||
pub fn with_crossorigin(mut self) -> Self {
|
||||
self.crossorigin = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Especifica el tipo MIME del recurso (atributo `type`).
|
||||
///
|
||||
/// Permite que el navegador omita la descarga si no soporta el formato indicado. Los
|
||||
/// constructores [`Self::font()`], [`Self::webp()`] y [`Self::avif()`] lo establecen
|
||||
/// automáticamente; este método cubre otros formatos o permite cambiar el valor por defecto.
|
||||
pub fn with_mime_type(mut self, mime: impl Into<CowStr>) -> Self {
|
||||
self.mime_type = mime.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Asset for Preload {
|
||||
fn name(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn weight(&self) -> Weight {
|
||||
self.weight
|
||||
}
|
||||
|
||||
fn render(&self, _cx: &mut Context) -> Markup {
|
||||
html! {
|
||||
link
|
||||
rel="preload"
|
||||
href=(util::join_pair!(&self.path, "?v=", &self.version))
|
||||
as=(self.as_type.as_str())
|
||||
type=[(!self.mime_type.is_empty()).then_some(self.mime_type.as_ref())]
|
||||
crossorigin[self.crossorigin];
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue