Cada una lleva la suya: la del 404 y la del 500, recortadas del original —traían una banda plana pegada a la derecha, de generarlas— y pasadas a webp, 95 y 70 KB. El encabezado no repite lo que la lámina ya lleva dibujado: dice lo mismo con otras palabras, para que no se lea dos veces seguidas y para que la página siga teniendo titular si la imagen no carga. La música suena con un botón, no sola. Los navegadores cortan el audio que empieza sin que el usuario haya tocado nada, y quien llega a un 404 desde un enlace roto no ha tocado nada: arrancarla al cargar no fallaría, es que no sonaría, y en silencio. Con `preload="none"`, además, el mp3 no se descarga hasta que se pide; sin eso, 686 KB a todo el que aterrice. Y suena una cosa cada vez. Al 404 no se puede llegar sin recarga entera —SvelteKit no enruta lo que no existe—, así que ahí nunca hay barra; al 500 sí, y entonces darle aquí pausa el tema, y darle al play de la barra para esto. Un 403 no es ninguna de las dos cosas: la página existe y el sitio funciona, es que no te toca. Esos salen sin ilustración, con su mensaje, en vez de con una lámina que miente. Y se descartan los mensajes que pone SvelteKit cuando no hay uno nuestro: «Internal Error» en un sitio en español es peor que no poner nada. Con `src/error.html`, que es lo que sale cuando la aplicación no puede ni pintar su propia página de error. Va desnudo a propósito: sin hoja de estilos, sin fuentes, sin imagen y sin JavaScript, porque todo lo que pidiera fuera podría estar caído también. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>master
parent
fef07e6b8b
commit
623eaa9bea
@ -0,0 +1,74 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#0e1424" />
|
||||||
|
<meta name="robots" content="noindex" />
|
||||||
|
<title>%sveltekit.status% · Senza Paura</title>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Esto sale cuando la aplicación no ha podido ni pintar su propia página de
|
||||||
|
error: un fallo antes de que arranque nada. Por eso va desnuda —sin hoja
|
||||||
|
de estilos, sin fuentes, sin imagen y sin JavaScript—: todo lo que se
|
||||||
|
pidiera fuera podría estar caído también, y entonces no se vería ni esto.
|
||||||
|
Los colores son los mismos del sitio, escritos a mano.
|
||||||
|
-->
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 2rem;
|
||||||
|
background: #0e1424;
|
||||||
|
color: #eef1f7;
|
||||||
|
font-family:
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
'Segoe UI',
|
||||||
|
Roboto,
|
||||||
|
sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
max-width: 44ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.codigo {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: #e2b567;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 1.5rem;
|
||||||
|
color: #aab3c5;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #e2b567;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<p class="codigo">%sveltekit.status%</p>
|
||||||
|
<h1>%sveltekit.error.message%</h1>
|
||||||
|
<p>
|
||||||
|
El sitio no ha podido cargarse. Vuelve a intentarlo en un momento; si sigue igual,
|
||||||
|
escríbenos a <a href="mailto:hola@senzapaura.es">hola@senzapaura.es</a>.
|
||||||
|
</p>
|
||||||
|
<p><a href="/">Volver al inicio</a></p>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -1,48 +1,297 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Las páginas de error, 404 y 500.
|
||||||
|
*
|
||||||
|
* Cada una lleva su lámina y su tema. La lámina trae el número y el rótulo
|
||||||
|
* dibujados dentro, así que la página **no** los repite: escribe lo que la
|
||||||
|
* imagen no puede decir —qué ha pasado y por dónde se sale— y deja el titular
|
||||||
|
* a la ilustración. Aun así hay un `h1` de verdad, porque una página cuyo
|
||||||
|
* único título viva dentro de un `.webp` no la lee un lector de pantalla ni
|
||||||
|
* sobrevive a que la imagen no cargue.
|
||||||
|
*
|
||||||
|
* ## La música
|
||||||
|
*
|
||||||
|
* Suena con un botón, no sola. Los navegadores cortan el audio que empieza
|
||||||
|
* sin que el usuario haya tocado nada, y quien llega a un 404 desde un enlace
|
||||||
|
* roto no ha tocado nada todavía: arrancarla al cargar no fallaría, es que no
|
||||||
|
* sonaría, y en silencio.
|
||||||
|
*
|
||||||
|
* Y suena una cosa cada vez. Si había un tema en la barra de reproducción, se
|
||||||
|
* pausa al dar aquí; si desde la barra se vuelve a dar al play, esto se para.
|
||||||
|
* Manda el último que haya pulsado, que es lo que uno espera.
|
||||||
|
*
|
||||||
|
* El `preload="none"` importa: sin él, el navegador se traería los 686 KB del
|
||||||
|
* mp3 a todo el que aterrice en un 404, lo vaya a escuchar o no.
|
||||||
|
*/
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
|
import Icono from '$lib/components/Icono.svelte';
|
||||||
import Meta from '$lib/components/Meta.svelte';
|
import Meta from '$lib/components/Meta.svelte';
|
||||||
|
import { reproductor } from '$lib/player/player.svelte';
|
||||||
|
|
||||||
const titulo = $derived(page.status === 404 ? 'Página no encontrada' : 'Ha ocurrido un error');
|
/**
|
||||||
|
* Lo que pone SvelteKit cuando no hay mensaje nuestro. Están en inglés y no
|
||||||
|
* dicen nada —«Internal Error» en un sitio en español es peor que no poner
|
||||||
|
* nada—, así que se descartan y se escribe la frase a mano. Los que sí son
|
||||||
|
* nuestros vienen redactados: «Ese tema no existe.», «Esta cuenta ya no
|
||||||
|
* administra el sitio.»
|
||||||
|
*/
|
||||||
|
const GENERICOS = new Set([
|
||||||
|
'Not Found',
|
||||||
|
'Internal Error',
|
||||||
|
'Forbidden',
|
||||||
|
'Unauthorized',
|
||||||
|
'Bad Request'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const mensaje = $derived(
|
||||||
|
page.error?.message && !GENERICOS.has(page.error.message) ? page.error.message : null
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Solo hay lámina para dos casos, y son los dos que tienen dibujo: la que dice
|
||||||
|
* «página no encontrada» y la que dice «algo ha salido mal». Un 403 no es
|
||||||
|
* ninguna de las dos cosas —la página existe y el sitio funciona, es que no
|
||||||
|
* te toca—, así que sale sin ilustración en vez de con una que miente.
|
||||||
|
*/
|
||||||
|
const caso = $derived(page.status === 404 ? 'perdida' : page.status >= 500 ? 'rota' : 'otra');
|
||||||
|
const lamina = $derived(caso === 'perdida' ? '404' : caso === 'rota' ? '500' : null);
|
||||||
|
|
||||||
|
/** El de la pestaña y el buscador; el de la página va aparte, más abajo. */
|
||||||
|
const titulo = $derived(
|
||||||
|
caso === 'perdida'
|
||||||
|
? 'Página no encontrada'
|
||||||
|
: caso === 'rota'
|
||||||
|
? 'Algo ha salido mal'
|
||||||
|
: (mensaje ?? `Error ${page.status}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* El encabezado no repite lo que la lámina ya lleva dibujado —«404 · PÁGINA
|
||||||
|
* NO ENCONTRADA»—, dice lo mismo con otras palabras. Así no se lee dos veces
|
||||||
|
* seguidas, y si la imagen no carga la página sigue teniendo su titular.
|
||||||
|
*/
|
||||||
|
const encabezado = $derived(
|
||||||
|
caso === 'perdida'
|
||||||
|
? 'Esta dirección no lleva a ninguna parte'
|
||||||
|
: caso === 'rota'
|
||||||
|
? 'Se ha roto por nuestra parte'
|
||||||
|
: (mensaje ?? `Error ${page.status}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const detalle = $derived(
|
||||||
|
caso === 'perdida'
|
||||||
|
? (mensaje ?? 'O nunca existió, o el contenido se ha movido de sitio.')
|
||||||
|
: caso === 'rota'
|
||||||
|
? 'No es cosa tuya. Vuelve a intentarlo en un momento y, si sigue igual, avísanos.'
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
let audio = $state<HTMLAudioElement | null>(null);
|
||||||
|
let sonando = $state(false);
|
||||||
|
|
||||||
|
function alternar(): void {
|
||||||
|
if (!audio) return;
|
||||||
|
if (sonando) {
|
||||||
|
audio.pause();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Una cosa cada vez: lo que hubiera en la barra se calla.
|
||||||
|
if (reproductor.sonando) reproductor.pausar();
|
||||||
|
void audio.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* El otro lado del trato: si desde la barra vuelven a dar al play, esto se
|
||||||
|
* calla. `untrack` no hace falta —solo se lee `reproductor.sonando`—, pero sí
|
||||||
|
* la guarda de `sonando`, o el efecto pausaría un audio ya parado en cada
|
||||||
|
* cambio del reproductor.
|
||||||
|
*/
|
||||||
|
$effect(() => {
|
||||||
|
if (reproductor.sonando && sonando) audio?.pause();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Meta {titulo} sinIndexar />
|
<Meta {titulo} sinIndexar />
|
||||||
|
|
||||||
<div class="container error">
|
<div class="container error" class:sola={!lamina}>
|
||||||
|
{#if lamina}
|
||||||
|
<figure class="lamina">
|
||||||
|
<img
|
||||||
|
src="/errores/{lamina}.webp"
|
||||||
|
alt={caso === 'perdida'
|
||||||
|
? 'Una chica sentada junto a la funda de una guitarra, mirando hacia arriba, bajo un 404 dorado.'
|
||||||
|
: 'Un 500 dorado sobre el fondo marino, entre líneas y notas sueltas.'}
|
||||||
|
width="1200"
|
||||||
|
height="1364"
|
||||||
|
fetchpriority="high"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="dicho">
|
||||||
<p class="codigo muted">{page.status}</p>
|
<p class="codigo muted">{page.status}</p>
|
||||||
<h1>{titulo}</h1>
|
<h1>{encabezado}</h1>
|
||||||
<p class="detalle">
|
{#if detalle}
|
||||||
{#if page.status === 404}
|
<p class="detalle">{detalle}</p>
|
||||||
La dirección a la que has llegado no existe o el contenido se ha movido.
|
|
||||||
{:else}
|
|
||||||
{page.error?.message ?? 'Algo ha fallado por nuestra parte.'}
|
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
|
||||||
<p class="acciones">
|
<div class="acciones">
|
||||||
<a class="btn btn--primary" href={resolve('/')}>Volver al inicio</a>
|
<a class="btn btn--primary" href={resolve('/')}>Volver al inicio</a>
|
||||||
|
{#if caso === 'perdida'}
|
||||||
<a class="btn" href={resolve('/musica')}>Ir a la música</a>
|
<a class="btn" href={resolve('/musica')}>Ir a la música</a>
|
||||||
|
{:else if caso === 'rota'}
|
||||||
|
<a class="btn" href={resolve('/contacto')}>Avisarnos</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if lamina}
|
||||||
|
<div class="musica">
|
||||||
|
<audio
|
||||||
|
bind:this={audio}
|
||||||
|
preload="none"
|
||||||
|
src="/errores/{lamina}.mp3"
|
||||||
|
onplay={() => (sonando = true)}
|
||||||
|
onpause={() => (sonando = false)}
|
||||||
|
onended={() => (sonando = false)}
|
||||||
|
></audio>
|
||||||
|
|
||||||
|
<button type="button" class="poner" onclick={alternar}>
|
||||||
|
<Icono nombre={sonando ? 'pausa' : 'play'} tamano={18} />
|
||||||
|
{sonando ? 'Parar' : 'Poner música'}
|
||||||
|
</button>
|
||||||
|
<p class="mientras muted">
|
||||||
|
{caso === 'perdida' ? 'Ya que has llegado hasta aquí.' : 'Mientras lo arreglamos.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/*
|
||||||
|
* La lámina es vertical (0,88:1) y manda el alto; el texto se centra a su
|
||||||
|
* lado en vez de colgar de arriba, que con una imagen tan alta dejaría el
|
||||||
|
* bloque flotando en la esquina.
|
||||||
|
*
|
||||||
|
* El bloque se limita a 64rem y se centra en vez de estirarse a lo ancho del
|
||||||
|
* contenedor: las dos columnas suman justo eso —30rem de lámina y las 44ch
|
||||||
|
* del texto—, y dejarlas sueltas en un monitor ancho abriría un hueco a la
|
||||||
|
* derecha del texto que no hay con qué llenar.
|
||||||
|
*/
|
||||||
.error {
|
.error {
|
||||||
padding-block: var(--sp-8);
|
display: grid;
|
||||||
max-width: 44ch;
|
grid-template-columns: minmax(0, 30rem) minmax(0, 1fr);
|
||||||
|
gap: var(--sp-7);
|
||||||
|
align-items: center;
|
||||||
|
max-width: 64rem;
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-block: var(--sp-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sin lámina —un 403, un 401— no hay dos columnas que repartir. */
|
||||||
|
.error.sola {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
max-width: 44rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lamina {
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.codigo {
|
.codigo {
|
||||||
|
margin-bottom: var(--sp-2);
|
||||||
font-size: var(--fs-sm);
|
font-size: var(--fs-sm);
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
margin-bottom: var(--sp-2);
|
}
|
||||||
|
|
||||||
|
.lamina img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: var(--radius-tarjeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dicho {
|
||||||
|
max-width: 44ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detalle {
|
.detalle {
|
||||||
font-size: var(--fs-lg);
|
font-size: var(--fs-lg);
|
||||||
|
color: var(--c-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.acciones {
|
.acciones {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: var(--sp-3);
|
gap: var(--sp-3);
|
||||||
|
margin-top: var(--sp-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Separada de los botones de salir: esto no es una forma de irse. */
|
||||||
|
.musica {
|
||||||
|
margin-top: var(--sp-7);
|
||||||
|
padding-top: var(--sp-5);
|
||||||
|
border-top: 1px solid var(--c-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.poner {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
padding: var(--boton-relleno);
|
||||||
|
border: 1px solid var(--c-border);
|
||||||
|
border-radius: var(--radius-boton);
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font-size: var(--boton-texto);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.3s var(--suave);
|
||||||
|
}
|
||||||
|
|
||||||
|
.poner:hover,
|
||||||
|
.poner:focus-visible {
|
||||||
|
border-color: var(--c-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mientras {
|
||||||
|
margin: var(--sp-2) 0 0;
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.poner {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Apiladas, la lámina se limita: a ancho completo en un móvil ocuparía la
|
||||||
|
* pantalla entera y el texto quedaría por debajo del pliegue, que es justo lo
|
||||||
|
* que hace falta leer.
|
||||||
|
*/
|
||||||
|
@media (max-width: 52rem) {
|
||||||
|
.error {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: var(--sp-5);
|
||||||
|
justify-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lamina {
|
||||||
|
max-width: 20rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.acciones {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.musica {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
Loading…
Reference in new issue