Make server error pages resilient

codex/redisenio-dominio-musical
dev 4 weeks ago
parent ae9f9bb5c5
commit 988a874b72

@ -57,13 +57,20 @@ test.describe('Navegación', () => {
test('cada estilo tiene su propia página, con su propio título', async ({ page }) => {
await page.goto('/musica');
await page.getByRole('heading', { level: 2, name: 'Clásica' }).getByRole('link').click();
await expect(page).toHaveURL('/musica/clasica');
const enlace = page.getByRole('heading', { level: 2 }).first().getByRole('link');
const titulo = (await enlace.textContent())?.trim();
const destino = await enlace.getAttribute('href');
await enlace.click();
expect(titulo).toBeTruthy();
expect(destino).toBeTruthy();
await expect(page).toHaveURL(destino!);
// En su página el estilo es el h1, no un h2 heredado del listado: es de lo
// que va la página.
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Clásica');
await expect(page.getByRole('link', { name: 'Preludios para piano' }).first()).toBeVisible();
await expect(page.getByRole('heading', { level: 1 })).toHaveText(titulo!);
await expect(
page.locator('a[href^="/albumes/"], a[href^="/canciones/"]').first()
).toBeVisible();
});
test('la página de un estilo lleva al lado todos los demás', async ({ page }) => {
@ -80,14 +87,21 @@ test.describe('Navegación', () => {
});
test('desde un estilo se llega al álbum y del álbum al tema', async ({ page }) => {
await page.goto('/musica/baladas-romanticas');
await page.goto('/musica');
await page.getByRole('heading', { level: 2 }).first().getByRole('link').click();
await page.getByRole('link', { name: 'Como si nada' }).first().click();
await expect(page).toHaveURL('/albumes/como-si-nada');
const enlaceAlbum = page.locator('a[href^="/albumes/"]').first();
const album = await enlaceAlbum.getAttribute('href');
expect(album).toBeTruthy();
await enlaceAlbum.click();
await expect(page).toHaveURL(album!);
await expect(page.getByRole('heading', { level: 2, name: 'Temas' })).toBeVisible();
await page.getByRole('link', { name: 'Media verdad' }).click();
await expect(page).toHaveURL('/canciones/media-verdad');
const enlaceCancion = page.locator('a[href^="/canciones/"]').first();
const cancion = await enlaceCancion.getAttribute('href');
expect(cancion).toBeTruthy();
await enlaceCancion.click();
await expect(page).toHaveURL(cancion!);
});
test('una dirección inexistente muestra la página de error', async ({ page }) => {

@ -3,72 +3,209 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#0e1424" />
<meta name="theme-color" content="#000719" />
<meta name="robots" content="noindex" />
<title>%sveltekit.status% · Senza Paura</title>
<title>Error %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.
Último recurso para fallos anteriores al renderizado de Svelte. No depende
de CSS, fuentes, imágenes ni JavaScript externos: si la aplicación no ha
podido arrancar, esta salida todavía debe funcionar por sí sola.
-->
<style>
body {
margin: 0;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 2rem;
background: #0e1424;
color: #eef1f7;
:root {
color-scheme: dark;
font-family:
system-ui,
-apple-system,
'Segoe UI',
Roboto,
sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
display: grid;
place-items: center;
min-height: 100vh;
min-height: 100dvh;
padding: clamp(1rem, 3vw, 2.5rem);
background:
radial-gradient(circle at 12% 18%, rgba(27, 71, 111, 0.3), transparent 34rem), #000719;
color: #eef1f7;
line-height: 1.6;
}
main {
max-width: 44ch;
display: grid;
grid-template-columns: minmax(18rem, 0.9fr) minmax(24rem, 1.1fr);
width: min(76rem, 100%);
min-height: min(42rem, calc(100dvh - 5rem));
border: 1px solid rgba(226, 181, 103, 0.25);
box-shadow: 0 2rem 6rem rgba(0, 0, 0, 0.36);
}
.lamina {
position: relative;
display: grid;
place-items: center;
overflow: hidden;
min-height: 28rem;
background:
linear-gradient(
135deg,
transparent 48%,
rgba(226, 181, 103, 0.12) 48.1% 48.4%,
transparent 48.5%
),
radial-gradient(circle at 50% 42%, #0c2b4b 0, #00122a 48%, #000719 100%);
}
.lamina::after {
content: '';
position: absolute;
inset: 1.25rem;
border: 1px solid rgba(238, 241, 247, 0.12);
pointer-events: none;
}
p.codigo {
.marca {
position: absolute;
top: 2rem;
left: 2rem;
margin: 0;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.2em;
text-transform: uppercase;
color: #e2b567;
}
.numero {
position: relative;
margin: 0;
font-family: Georgia, 'Times New Roman', serif;
font-size: clamp(8rem, 18vw, 15rem);
font-weight: 400;
line-height: 0.8;
letter-spacing: -0.08em;
color: #eef1f7;
text-shadow: 0 0 4rem rgba(125, 177, 220, 0.28);
}
.contenido {
display: flex;
flex-direction: column;
justify-content: center;
padding: clamp(2.25rem, 6vw, 5.5rem);
background: #00213c;
}
.codigo {
margin: 0 0 0.5rem;
font-size: 0.875rem;
letter-spacing: 0.08em;
font-size: 0.75rem;
font-weight: 650;
letter-spacing: 0.16em;
text-transform: uppercase;
color: #e2b567;
}
h1 {
margin: 0 0 1rem;
font-size: 1.75rem;
font-weight: 500;
max-width: 12ch;
margin: 0;
font-family: Georgia, 'Times New Roman', serif;
font-size: clamp(2.25rem, 5vw, 4.5rem);
font-weight: 400;
line-height: 1.02;
text-wrap: balance;
}
p {
margin: 0 0 1.5rem;
color: #aab3c5;
.detalle {
max-width: 43ch;
margin: 1.4rem 0 0;
font-size: 1.05rem;
color: #c2cbdb;
}
a {
color: #e2b567;
.acciones {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-top: 2rem;
}
.acciones a {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 2.8rem;
padding: 0.7rem 1.2rem;
border: 1px solid #64748b;
border-radius: 0.25rem;
color: #eef1f7;
font-size: 0.92rem;
font-weight: 650;
text-decoration: none;
}
.acciones a:first-child {
border-color: #e2b567;
background: #e2b567;
color: #00122a;
}
.acciones a:focus-visible {
outline: 3px solid #eef1f7;
outline-offset: 3px;
}
@media (max-width: 47rem) {
body {
padding: 0;
}
main {
grid-template-columns: 1fr;
min-height: 100dvh;
border: 0;
}
.lamina {
min-height: 40dvh;
}
.numero {
font-size: clamp(7rem, 34vw, 11rem);
}
.contenido {
padding: 2.25rem 1.5rem 3rem;
}
}
</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>
<section class="lamina" aria-hidden="true">
<p class="marca">senzapaura.es</p>
<p class="numero">%sveltekit.status%</p>
</section>
<section class="contenido">
<p class="codigo">Error %sveltekit.status%</p>
<h1>Algo ha salido mal</h1>
<p class="detalle">
No es cosa tuya. No hemos podido arrancar bien el sitio; vuelve a intentarlo en un
momento.
</p>
<nav class="acciones" aria-label="Salidas del error">
<a href="">Reintentar</a>
<a href="/">Volver al inicio</a>
</nav>
</section>
</main>
</body>
</html>

@ -49,7 +49,7 @@
{#if enPagina}
{estilo.nombre}
{:else}
<a href={resolve('/musica/[estilo]', { estilo: estilo.slug })}>{estilo.nombre}</a>
<a href={resolve('/(sitio)/musica/[estilo]', { estilo: estilo.slug })}>{estilo.nombre}</a>
{/if}
</svelte:element>
{#if estilo.resumen}

@ -348,7 +348,7 @@
<ul class="temas">
{#each temas as tema (tema.slug)}
<li>
<a href={resolve('/etiquetas/[etiqueta]', { etiqueta: tema.slug })}>{tema.nombre}</a>
<a href={resolve('/(sitio)/etiquetas/[etiqueta]', { etiqueta: tema.slug })}>{tema.nombre}</a>
</li>
{/each}
</ul>

@ -46,7 +46,7 @@
<a
class="etiqueta"
style="--tono: {tonoDeEtiqueta(etiqueta)}"
href={resolve('/etiquetas/[etiqueta]', { etiqueta: aSlug(etiqueta) })}
href={resolve('/(sitio)/etiquetas/[etiqueta]', { etiqueta: aSlug(etiqueta) })}
>
{etiqueta}
</a>

@ -70,7 +70,7 @@
{#if album}
<dt>{album.sintetico ? 'Publicación' : 'Álbum'}</dt>
<dd>
<a href={resolve('/albumes/[slug]', { slug: album.slug })}>
<a href={resolve('/(sitio)/albumes/[slug]', { slug: album.slug })}>
{album.sintetico ? 'Single suelto' : album.titulo}
</a>
</dd>
@ -99,7 +99,7 @@
{#if estilo}
<dt>Estilo</dt>
<dd>
<a href={resolve('/musica/[estilo]', { estilo: estilo.slug })}>
<a href={resolve('/(sitio)/musica/[estilo]', { estilo: estilo.slug })}>
{estilo.nombre}
</a>
</dd>
@ -130,7 +130,7 @@
{#if i > 0},
{/if}
{#if interprete.slug}
<a href={resolve('/interpretes/[slug]', { slug: interprete.slug })}
<a href={resolve('/(sitio)/interpretes/[slug]', { slug: interprete.slug })}
>{interprete.nombre}</a
>
{:else}

@ -153,12 +153,12 @@
-->
<p class="letra-pequena muted">
Al entrar aceptas las <a
href={resolve('/legal/[slug]', { slug: 'condiciones-de-uso' })}
href={resolve('/(sitio)/legal/[slug]', { slug: 'condiciones-de-uso' })}
target="_blank"
rel="noopener">condiciones de uso</a
>
y la
<a href={resolve('/legal/[slug]', { slug: 'privacidad' })} target="_blank" rel="noopener"
<a href={resolve('/(sitio)/legal/[slug]', { slug: 'privacidad' })} target="_blank" rel="noopener"
>política de privacidad</a
>.
</p>

@ -89,7 +89,7 @@
variante="solido"
tamano={18}
/>
<a class="btn" href={resolve('/albumes/[slug]', { slug: album.slug })}>Ver el disco</a>
<a class="btn" href={resolve('/(sitio)/albumes/[slug]', { slug: album.slug })}>Ver el disco</a>
<a class="btn" href={resolve('/musica')}>Toda la música</a>
</div>

@ -52,7 +52,7 @@
{#each generos as genero (genero.slug)}
<li>
<a
href={resolve('/generos/[slug]', { slug: genero.slug })}
href={resolve('/(sitio)/generos/[slug]', { slug: genero.slug })}
aria-current={genero.slug === actual ? 'page' : undefined}
>
{genero.titulo}

@ -38,7 +38,7 @@
{#each principios as capitulo, i (capitulo.slug)}
<li>
<a
href={resolve('/taller/[slug]', { slug: capitulo.slug })}
href={resolve('/(sitio)/taller/[slug]', { slug: capitulo.slug })}
aria-current={capitulo.slug === actual ? 'page' : undefined}
>
<span class="numero" aria-hidden="true">{String(i + 1).padStart(2, '0')}</span>

@ -59,7 +59,7 @@
<ul>
{#each page.data.legales ?? [] as pagina (pagina.slug)}
<li>
<a href={resolve('/legal/[slug]', { slug: pagina.slug })}>{pagina.titulo}</a>
<a href={resolve('/(sitio)/legal/[slug]', { slug: pagina.slug })}>{pagina.titulo}</a>
</li>
{/each}
</ul>

@ -306,7 +306,7 @@
<td class="control">
<a
class="mini"
href={resolve('/canciones/[slug]', { slug: c.slug })}
href={resolve('/(sitio)/canciones/[slug]', { slug: c.slug })}
tabindex="-1"
onmouseenter={(e) => asomar(e, c)}
onmouseleave={() => (vista = null)}
@ -324,7 +324,7 @@
{/if}
<th scope="row">
<a class="titulo" href={resolve('/canciones/[slug]', { slug: c.slug })}>{c.titulo}</a>
<a class="titulo" href={resolve('/(sitio)/canciones/[slug]', { slug: c.slug })}>{c.titulo}</a>
<!--
De qué está hecho el tema, no su primer verso: la letra está
entera en la ficha, y aquí lo que sirve para elegir es el género
@ -335,7 +335,7 @@
<span class="marca">Instrumental</span>
{/if}
{#each c.generos as g (g.slug)}
<a class="genero" href={resolve('/generos/[slug]', { slug: g.slug })}>
<a class="genero" href={resolve('/(sitio)/generos/[slug]', { slug: g.slug })}>
{g.nombre}
</a>
{/each}
@ -358,7 +358,7 @@
<td class:numerica={columna.numerica}>
{#if columna.campo === 'estilo'}
{#if fila.estilo}
<a href={resolve('/musica/[estilo]', { estilo: fila.estilo.slug })}>
<a href={resolve('/(sitio)/musica/[estilo]', { estilo: fila.estilo.slug })}>
{fila.estilo.nombre}
</a>
{:else}
@ -366,7 +366,7 @@
{/if}
{:else if columna.campo === 'album'}
{#if fila.album}
<a href={resolve('/albumes/[slug]', { slug: fila.album.slug })}>
<a href={resolve('/(sitio)/albumes/[slug]', { slug: fila.album.slug })}>
{fila.album.titulo}
</a>
{:else}

@ -53,7 +53,7 @@
<div class="tarjeta__cuerpo">
<h3 class="titulo">
<a class="tarjeta__enlace" href={resolve('/albumes/[slug]', { slug: album.slug })}>
<a class="tarjeta__enlace" href={resolve('/(sitio)/albumes/[slug]', { slug: album.slug })}>
{album.titulo}
</a>
</h3>

@ -40,7 +40,7 @@
const album = $derived(obtenerAlbum(albumDeCancion(cancion)));
const estilo = $derived(obtenerEstilo(cancion.estilo));
const enlace = $derived(resolve('/canciones/[slug]', { slug: cancion.slug }));
const enlace = $derived(resolve('/(sitio)/canciones/[slug]', { slug: cancion.slug }));
</script>
<article class="tarjeta" use:revelar={retraso} use:brillo>

@ -46,7 +46,7 @@
<div class="tarjeta__cuerpo">
<h3 class="titulo">
<a class="tarjeta__enlace" href={resolve('/musica/[estilo]', { estilo: estilo.slug })}>
<a class="tarjeta__enlace" href={resolve('/(sitio)/musica/[estilo]', { estilo: estilo.slug })}>
{estilo.nombre}
</a>
</h3>

@ -111,16 +111,16 @@
{/if}
<div class="datos">
<p class="linea">
<a class="titulo" href={resolve('/canciones/[slug]', { slug: pista.cancionSlug })}>
<a class="titulo" href={resolve('/(sitio)/canciones/[slug]', { slug: pista.cancionSlug })}>
{pista.titulo}
</a>
<span class="calidad">Calidad de escucha</span>
</p>
<span class="contexto muted">
<a href={resolve('/albumes/[slug]', { slug: pista.albumSlug })}>{pista.albumTitulo}</a>
<a href={resolve('/(sitio)/albumes/[slug]', { slug: pista.albumSlug })}>{pista.albumTitulo}</a>
{#if pista.estiloNombre}
<span aria-hidden="true">&middot;</span>
<a href={resolve('/musica/[estilo]', { estilo: pista.estiloSlug })}
<a href={resolve('/(sitio)/musica/[estilo]', { estilo: pista.estiloSlug })}
>{pista.estiloNombre}</a
>
{/if}

@ -4,7 +4,6 @@ import { comprasDeUsuario } from '$lib/server/pedidos';
import { proveedoresDisponibles } from '$lib/server/oauth';
import { indiceLegal } from '$lib/server/legal';
import { leerCatalogo } from '$lib/server/catalogo';
import { RUTA_ESPERA } from '$lib/rutas';
/**
* Estado de la cuenta disponible en todas las páginas.
@ -14,28 +13,16 @@ import { RUTA_ESPERA } from '$lib/rutas';
* ofrecerlo otra vez. Al resolverlo aquí, cualquier componente puede leerlo de
* `page.data` sin que haya que ir pasándolo por props.
*
* Vive en el grupo `(sitio)`, un nivel por debajo del layout raíz. Esta
* separación es una frontera de errores: si falla la base de datos, el layout
* raíz sigue disponible y SvelteKit puede renderizar `+error.svelte` con el
* estado 500 correcto. Poner estas consultas en la raíz obligaría a caer en el
* HTML estático de último recurso.
*
* Que este `load` exista es lo que impide prerenderizar el sitio: a partir de
* la tienda, cada página depende de quién la pide.
*/
export async function load({ locals, cookies, url }) {
/*
* La página de espera no lleva cabecera ni carrito, así que nada de esto le
* hace falta. Sin este atajo, cada petición de fuera —incluidas las de los
* rastreadores, que son casi todas— abría tres consultas a la base de datos
* para pintar un cartel que no las usa.
*/
if (url.pathname === RUTA_ESPERA) {
return {
usuario: null,
carrito: [] as string[],
compras: [] as string[],
favoritos: [] as string[],
proveedores: [],
legales: [],
catalogo: { estilos: [], albumes: [], canciones: [] }
};
}
export async function load({ locals, cookies }) {
const usuario = locals.usuario;
const [carrito, compras, favoritos, legales, catalogo] = await Promise.all([
leerCarrito(cookies, usuario?.id ?? null),

@ -130,7 +130,7 @@
tamano={18}
/>
{/if}
<a class="btn" href={resolve('/musica/[estilo]', { estilo: principal.estilo.slug })}>
<a class="btn" href={resolve('/(sitio)/musica/[estilo]', { estilo: principal.estilo.slug })}>
Ver el estilo
</a>
</div>

@ -41,7 +41,7 @@
? [
{
texto: data.estilo.nombre,
href: resolve('/musica/[estilo]', { estilo: data.estilo.slug })
href: resolve('/(sitio)/musica/[estilo]', { estilo: data.estilo.slug })
}
]
: []),

@ -57,7 +57,7 @@
<ul aria-labelledby="por-estilo">
{#each data.obra as estilo (estilo.slug)}
<li>
<a href={resolve('/musica/[estilo]', { estilo: estilo.slug })}>
<a href={resolve('/(sitio)/musica/[estilo]', { estilo: estilo.slug })}>
<span>{estilo.nombre}</span>
<span class="cuantas muted">{plural(estilo.cuantas, 'tema', 'temas')}</span>
</a>

@ -171,7 +171,7 @@
<time datetime={entrada.fecha}>{formatearFecha(entrada.fecha)}</time>
</td>
<td>
<a href={resolve('/blog/[slug]', { slug: entrada.slug })}>{entrada.titulo}</a>
<a href={resolve('/(sitio)/blog/[slug]', { slug: entrada.slug })}>{entrada.titulo}</a>
</td>
<td class="celda-resumen muted">{entrada.resumen}</td>
<td class="celda-etiquetas muted">{entrada.etiquetas.join(', ')}</td>
@ -185,7 +185,7 @@
{#each encontradas as entrada, i (entrada.slug)}
<li class="tarjeta" use:revelar={Math.min(i, 8) * 45} use:brillo>
<article>
<a class="portada" href={resolve('/blog/[slug]', { slug: entrada.slug })} tabindex="-1">
<a class="portada" href={resolve('/(sitio)/blog/[slug]', { slug: entrada.slug })} tabindex="-1">
<img src={entrada.imagen} alt="" loading="lazy" />
</a>
@ -194,7 +194,7 @@
<time datetime={entrada.fecha}>{formatearFecha(entrada.fecha)}</time>
</p>
<h2>
<a href={resolve('/blog/[slug]', { slug: entrada.slug })}>{entrada.titulo}</a>
<a href={resolve('/(sitio)/blog/[slug]', { slug: entrada.slug })}>{entrada.titulo}</a>
</h2>
{#if entrada.resumen}
<p class="resumen muted">{entrada.resumen}</p>

@ -59,7 +59,7 @@
{#if data.anterior || data.siguiente}
<nav class="vecinas" aria-label="Otras entradas">
{#if data.anterior}
<a class="vecina" href={resolve('/blog/[slug]', { slug: data.anterior.slug })}>
<a class="vecina" href={resolve('/(sitio)/blog/[slug]', { slug: data.anterior.slug })}>
<span class="muted">Anterior</span>
<span>{data.anterior.titulo}</span>
</a>
@ -68,7 +68,7 @@
{/if}
{#if data.siguiente}
<a class="vecina derecha" href={resolve('/blog/[slug]', { slug: data.siguiente.slug })}>
<a class="vecina derecha" href={resolve('/(sitio)/blog/[slug]', { slug: data.siguiente.slug })}>
<span class="muted">Siguiente</span>
<span>{data.siguiente.titulo}</span>
</a>

@ -99,12 +99,12 @@
? [
{
texto: data.estilo.nombre,
href: resolve('/musica/[estilo]', { estilo: data.estilo.slug })
href: resolve('/(sitio)/musica/[estilo]', { estilo: data.estilo.slug })
}
]
: []),
...(data.album
? [{ texto: data.album.titulo, href: resolve('/albumes/[slug]', { slug: data.album.slug }) }]
? [{ texto: data.album.titulo, href: resolve('/(sitio)/albumes/[slug]', { slug: data.album.slug }) }]
: []),
{ texto: cancion.titulo }
]);
@ -138,12 +138,12 @@
-->
{#if data.album?.sintetico && data.estilo}
Single del estilo
<a href={resolve('/musica/[estilo]', { estilo: data.estilo.slug })}>
<a href={resolve('/(sitio)/musica/[estilo]', { estilo: data.estilo.slug })}>
{data.estilo.nombre}
</a>
{:else if data.album}
Del disco
<a href={resolve('/albumes/[slug]', { slug: data.album.slug })}>{data.album.titulo}</a>
<a href={resolve('/(sitio)/albumes/[slug]', { slug: data.album.slug })}>{data.album.titulo}</a>
{/if}
{#if cancion.ano}
<span aria-hidden="true">&middot;</span> {cancion.ano}
@ -267,7 +267,7 @@
{#each version.interpretes as interprete, j (`${interprete.id}-${interprete.rol}`)}
{#if j > 0},
{/if}{#if interprete.slug}<a
href={resolve('/interpretes/[slug]', { slug: interprete.slug })}
href={resolve('/(sitio)/interpretes/[slug]', { slug: interprete.slug })}
>{interprete.nombre}</a
>{:else}{interprete.nombre}{/if}
{/each}
@ -297,7 +297,7 @@
{#if data.anterior || data.siguiente}
<nav class="vecinos" aria-label="Otros temas del disco">
{#if data.anterior}
<a class="vecino" href={resolve('/canciones/[slug]', { slug: data.anterior.slug })}>
<a class="vecino" href={resolve('/(sitio)/canciones/[slug]', { slug: data.anterior.slug })}>
<span class="muted">Anterior</span>
<span>{data.anterior.titulo}</span>
</a>
@ -308,7 +308,7 @@
{#if data.siguiente}
<a
class="vecino derecha"
href={resolve('/canciones/[slug]', { slug: data.siguiente.slug })}
href={resolve('/(sitio)/canciones/[slug]', { slug: data.siguiente.slug })}
>
<span class="muted">Siguiente</span>
<span>{data.siguiente.titulo}</span>

@ -89,7 +89,7 @@
{/if}
<div class="datos">
<a class="titulo" href={resolve('/canciones/[slug]', { slug: cancion.slug })}>
<a class="titulo" href={resolve('/(sitio)/canciones/[slug]', { slug: cancion.slug })}>
{cancion.titulo}
</a>
<p class="contexto muted">
@ -180,7 +180,7 @@
<SinPortada titulo={cancion.titulo} lado={48} muda />
{/if}
<div class="datos">
<a class="titulo" href={resolve('/canciones/[slug]', { slug: cancion.slug })}>
<a class="titulo" href={resolve('/(sitio)/canciones/[slug]', { slug: cancion.slug })}>
{cancion.titulo}
</a>
<p class="contexto muted">

@ -21,7 +21,7 @@
<ul class="lista">
{#each data.items as item (item.slug)}
<li>
<a href={resolve('/canciones/[slug]', { slug: item.slug })}>{item.titulo}</a>
<a href={resolve('/(sitio)/canciones/[slug]', { slug: item.slug })}>{item.titulo}</a>
<a class="descarga" href={urlDescarga(item.slug)} download>Descargar</a>
</li>
{/each}

@ -80,7 +80,7 @@
{/if}
<div class="datos">
<a class="titulo" href={resolve('/canciones/[slug]', { slug: cancion.slug })}>
<a class="titulo" href={resolve('/(sitio)/canciones/[slug]', { slug: cancion.slug })}>
{cancion.titulo}
</a>
<p class="contexto muted">

@ -24,7 +24,7 @@
<ul class="nube">
{#each data.etiquetas as etiqueta (etiqueta.slug)}
<li>
<a href={resolve('/etiquetas/[etiqueta]', { etiqueta: etiqueta.slug })}>
<a href={resolve('/(sitio)/etiquetas/[etiqueta]', { etiqueta: etiqueta.slug })}>
{etiqueta.nombre}
<span class="cuenta muted">{etiqueta.canciones.length}</span>
</a>

@ -135,7 +135,7 @@
{#each generos as genero (genero.slug)}
{@const cuna = genero.paises.find((p) => p.relacion === 'cuna')}
<li>
<a href={resolve('/generos/[slug]', { slug: genero.slug })}>
<a href={resolve('/(sitio)/generos/[slug]', { slug: genero.slug })}>
<span class="nombre">{genero.nombre}</span>
<span class="origen muted">
{cuna?.nombre ?? genero.origenLugar ?? ''}{#if genero.origenAno}, {genero.origenAno}{/if}

@ -8,5 +8,5 @@
<Capitulo
capitulo={data.capitulo}
seccion={{ texto: 'Géneros', href: resolve('/generos') }}
enlaceA={(slug) => resolve('/generos/[slug]', { slug })}
enlaceA={(slug) => resolve('/(sitio)/generos/[slug]', { slug })}
/>

@ -100,7 +100,7 @@
<p class="generos">
{#each persona.generos as genero, i (genero.slug + genero.papel)}
{#if i > 0}<span class="separa" aria-hidden="true">·</span>{/if}
<a href={resolve('/generos/[slug]', { slug: genero.slug })}>{genero.nombre}</a>
<a href={resolve('/(sitio)/generos/[slug]', { slug: genero.slug })}>{genero.nombre}</a>
<span class="papel muted">{PAPEL[genero.papel] ?? genero.papel}</span>
{/each}
</p>

@ -22,7 +22,7 @@
<ul class="artistas">
{#each data.artistas as artista (artista.id)}
<li>
<a href={resolve('/interpretes/[slug]', { slug: artista.slug })}>
<a href={resolve('/(sitio)/interpretes/[slug]', { slug: artista.slug })}>
{#if artista.imagen}
<img src={artista.imagen} alt="" loading="lazy" />
{/if}

@ -23,7 +23,7 @@
<li class="tarjeta" use:revelar={i * 45} use:brillo>
<div class="tarjeta__cuerpo">
<span class="titulo">
<a class="tarjeta__enlace" href={resolve('/legal/[slug]', { slug: pagina.slug })}>
<a class="tarjeta__enlace" href={resolve('/(sitio)/legal/[slug]', { slug: pagina.slug })}>
{pagina.titulo}
</a>
</span>

@ -25,7 +25,7 @@
{#each data.legales as otra (otra.slug)}
<li>
<a
href={resolve('/legal/[slug]', { slug: otra.slug })}
href={resolve('/(sitio)/legal/[slug]', { slug: otra.slug })}
aria-current={otra.slug === pagina.slug ? 'page' : undefined}
>
{otra.titulo}

@ -28,7 +28,7 @@
{#each data.bloques as bloque (bloque.estilo.slug)}
<li>
<a
href={resolve('/musica/[estilo]', { estilo: bloque.estilo.slug })}
href={resolve('/(sitio)/musica/[estilo]', { estilo: bloque.estilo.slug })}
aria-current={bloque.estilo.slug === actual ? 'page' : undefined}
>
<span>{bloque.estilo.nombre}</span>

@ -31,7 +31,7 @@
<ol class="principios">
{#each data.principios as capitulo, i (capitulo.slug)}
<li use:revelar={Math.min(i, 8) * 45}>
<a href={resolve('/taller/[slug]', { slug: capitulo.slug })}>
<a href={resolve('/(sitio)/taller/[slug]', { slug: capitulo.slug })}>
<span class="numero" aria-hidden="true">{String(i + 1).padStart(2, '0')}</span>
<span class="cuerpo">
<span class="titulo">{capitulo.titulo}</span>

@ -8,5 +8,5 @@
<Capitulo
capitulo={data.capitulo}
seccion={{ texto: 'Taller de letras', href: resolve('/taller') }}
enlaceA={(slug) => resolve('/taller/[slug]', { slug })}
enlaceA={(slug) => resolve('/(sitio)/taller/[slug]', { slug })}
/>
Loading…
Cancel
Save

Powered by TurnKey Linux.