Los textos legales se sirven de la base, con su versión en vigor

Segunda área que deja `src/content`. El texto ya no es un archivo: es la
versión vigente de un documento, con la fecha en que entró en vigor. Eso
no es burocracia —quien compró en marzo se acogió a las condiciones de
marzo, y si hay una reclamación hay que poder demostrar qué decían—, y de
paso la fecha que se enseña deja de ser un campo que alguien puede
olvidarse de tocar.

Un documento sin versión vigente no sale en el índice, así que se puede
preparar un texto nuevo sin que aparezca en el pie a medias.

El pie los recibe del layout en vez de importarlos: un componente no
puede consultar. Y el mapa del sitio saca de la base tanto el taller como
los legales, en un solo `Promise.all`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
master
dev 4 weeks ago
parent b22d7fbff2
commit 2506e90bde

@ -1,7 +1,7 @@
<script lang="ts">
/** Pie del sitio: contacto directo, redes y aviso de autoria. */
import { resolve } from '$app/paths';
import { legales } from '$lib/content';
import { page } from '$app/state';
import { site } from '$lib/site';
const ano = new Date().getFullYear();
@ -57,7 +57,7 @@
-->
<nav aria-label="Legal">
<ul>
{#each legales as pagina (pagina.slug)}
{#each page.data.legales ?? [] as pagina (pagina.slug)}
<li>
<a href={resolve('/legal/[slug]', { slug: pagina.slug })}>{pagina.titulo}</a>
</li>

@ -27,18 +27,15 @@ import type {
EntradaDeBlog,
Estilo,
Etiqueta,
PaginaLegal,
PaginaMeta,
EstiloConObra
} from '$lib/types';
import { porFechaDesc } from '$lib/utils/date';
import { slugDesdeRuta } from './fields';
import { encabezadosDe, type Encabezado } from './anclas';
import {
parsearAlbum,
parsearCancion,
parsearEntrada,
parsearLegal,
parsearEstilo,
parsearPagina,
PORTADA_SINGLES,
@ -60,19 +57,6 @@ const modCanciones = import.meta.glob<ModuloMarkdown>('/src/content/canciones/*.
const modPaginas = import.meta.glob<ModuloMarkdown>('/src/content/paginas/*.md', { eager: true });
const modBlog = import.meta.glob<ModuloMarkdown>('/src/content/blog/*.md', { eager: true });
const modLegal = import.meta.glob<ModuloMarkdown>('/src/content/legal/*.md', { eager: true });
/*
* Los mismos textos legales, pero sin compilar. Se piden dos veces a proposito:
* de la version compilada sale el componente que se pinta, y de esta salen los
* encabezados del indice lateral. Cuando el cuerpo ya es un componente, el
* texto dejo de ser texto y no hay forma de recorrerlo.
*/
const crudoLegal = import.meta.glob('/src/content/legal/*.md', {
eager: true,
query: '?raw',
import: 'default'
}) as Record<string, string>;
/** Aplica un parser al frontmatter de cada archivo del glob. */
function mapear<T>(
@ -122,25 +106,14 @@ export const entradas: EntradaDeBlog[] = mapear(modBlog, parsearEntrada)
.filter((e) => !e.borrador)
.sort(porFechaDesc);
/** Los textos legales, en el orden en que se enumeran en el pie. */
export const legales: PaginaLegal[] = mapear(modLegal, parsearLegal).sort(
(a, b) => a.orden - b.orden
);
export function obtenerLegal(slug: string): PaginaLegal | undefined {
return legales.find((p) => p.slug === slug);
}
/** Componente con el texto de una pagina legal. */
export function cuerpoLegal(slug: string): Component | undefined {
return cuerpo(modLegal, slug);
}
/** Apartados de una pagina legal, para el indice lateral. */
export function indiceLegal(slug: string): Encabezado[] {
const entrada = Object.entries(crudoLegal).find(([ruta]) => slugDesdeRuta(ruta) === slug);
return entrada ? encabezadosDe(entrada[1]) : [];
}
/*
* Los textos legales tampoco se leen ya de aquí: están en `documento_legal` y
* sus versiones, y los sirve `$lib/server/legal`. El texto en vigor no es un
* archivo sino una versión con fecha, porque quien compró en marzo se acogió a
* las condiciones de marzo y eso hay que poder demostrarlo.
*
* Los `.md` siguen en `src/content/legal` porque son de donde salió el volcado.
*/
export function obtenerEntrada(slug: string): EntradaDeBlog | undefined {
return entradas.find((e) => e.slug === slug);

@ -0,0 +1,105 @@
/**
* Los textos legales, leídos de la base.
*
* El texto no vive en `documento_legal` sino en sus versiones, y eso no es
* burocracia: quien compró en marzo se acogió a las condiciones de marzo, y si
* hay una reclamación hay que poder demostrar qué decían entonces. Una versión
* publicada no se toca; cambiar el texto es publicar otra.
*
* De ahí sale también la fecha que se enseña: no es un campo escrito a mano que
* alguien puede olvidarse de actualizar, es cuándo entró en vigor lo que se
* está leyendo.
*/
import { asc, eq } from 'drizzle-orm';
import { db } from './db';
import { documentoLegal, versionLegal } from './db/schema';
import { componer } from './markdown';
import { encabezadosDe, type Encabezado } from '$lib/content/anclas';
/** Una entrada del índice legal y del pie de página. */
export interface EnElIndiceLegal {
slug: string;
titulo: string;
resumen: string;
orden: number;
/** ISO `YYYY-MM-DD` de la versión en vigor. */
actualizada: string;
}
/** Un documento con el texto de su versión vigente, ya compuesto. */
export interface DocumentoConTexto extends EnElIndiceLegal {
html: string;
apartados: Encabezado[];
/** Qué cambió respecto de la anterior, si se anotó. */
cambios: string | null;
}
/** Solo `YYYY-MM-DD`: la hora no significa nada en una fecha de vigencia. */
function enDia(fecha: Date): string {
return fecha.toISOString().slice(0, 10);
}
/**
* El índice: todos los documentos con versión en vigor.
*
* Un documento sin versión vigente no sale. Es lo que permite preparar un texto
* nuevo —crearlo y escribir su versión— sin que aparezca en el pie a medias.
*/
export async function indiceLegal(): Promise<EnElIndiceLegal[]> {
const filas = await db
.select({
slug: documentoLegal.slug,
titulo: documentoLegal.titulo,
resumen: documentoLegal.resumen,
orden: documentoLegal.orden,
vigenteDesde: versionLegal.vigenteDesde
})
.from(documentoLegal)
.innerJoin(versionLegal, eq(versionLegal.id, documentoLegal.versionVigenteId))
.orderBy(asc(documentoLegal.orden), asc(documentoLegal.titulo));
return filas.map((f) => ({
slug: f.slug,
titulo: f.titulo,
resumen: f.resumen,
orden: f.orden,
actualizada: enDia(f.vigenteDesde)
}));
}
export async function obtenerLegal(slug: string): Promise<DocumentoConTexto | null> {
const [fila] = await db
.select({
slug: documentoLegal.slug,
titulo: documentoLegal.titulo,
resumen: documentoLegal.resumen,
orden: documentoLegal.orden,
cuerpo: versionLegal.cuerpo,
cambios: versionLegal.resumenCambios,
vigenteDesde: versionLegal.vigenteDesde
})
.from(documentoLegal)
.innerJoin(versionLegal, eq(versionLegal.id, documentoLegal.versionVigenteId))
.where(eq(documentoLegal.slug, slug))
.limit(1);
if (!fila) return null;
return {
slug: fila.slug,
titulo: fila.titulo,
resumen: fila.resumen,
orden: fila.orden,
actualizada: enDia(fila.vigenteDesde),
html: componer(fila.cuerpo),
/*
* Los apartados salen del Markdown en bruto y con la misma función que
* pone las anclas al componer: así el índice y sus destinos no pueden
* separarse, que es un fallo que no da error, solo no lleva a ningún
* sitio.
*/
apartados: encabezadosDe(fila.cuerpo),
cambios: fila.cambios
};
}

@ -2,6 +2,7 @@ import { leerCarrito } from '$lib/server/carrito';
import { favoritosDeUsuario } from '$lib/server/favoritos';
import { comprasDeUsuario } from '$lib/server/pedidos';
import { proveedoresDisponibles } from '$lib/server/oauth';
import { indiceLegal } from '$lib/server/legal';
import { RUTA_ESPERA } from '$lib/rutas';
/**
@ -28,7 +29,8 @@ export async function load({ locals, cookies, url }) {
carrito: [] as string[],
compras: [] as string[],
favoritos: [] as string[],
proveedores: []
proveedores: [],
legales: []
};
}
@ -49,6 +51,13 @@ export async function load({ locals, cookies, url }) {
* diálogo de acceso, que puede abrirse desde cualquier página. Un botón
* de «Entrar con Google» sin configurar solo sirve para llevar a un error.
*/
proveedores: usuario ? [] : proveedoresDisponibles()
proveedores: usuario ? [] : proveedoresDisponibles(),
/*
* Los textos legales van en el pie, que está en todas las páginas. Antes
* los importaba el propio componente del catálogo de archivos; ahora que
* viven en la base tienen que bajar por aquí, porque un componente no
* puede consultar.
*/
legales: await indiceLegal()
};
}

@ -0,0 +1,9 @@
import { indiceLegal } from '$lib/server/legal';
/**
* Pasa a ser `+page.server.ts` porque consulta la base: el cargador universal
* corre también en el navegador, y allí no hay base a la que preguntar.
*/
export async function load() {
return { legales: await indiceLegal() };
}

@ -1,5 +0,0 @@
import { legales } from '$lib/content';
export function load() {
return { legales };
}

@ -0,0 +1,13 @@
import { error } from '@sveltejs/kit';
import { indiceLegal, obtenerLegal } from '$lib/server/legal';
export async function load({ params }) {
/*
* Las dos consultas a la vez: el índice acompaña al texto en la columna de
* al lado, así que hace falta siempre y encadenarlas sería un viaje de más.
*/
const [pagina, legales] = await Promise.all([obtenerLegal(params.slug), indiceLegal()]);
if (!pagina) error(404, 'Esa página no existe.');
return { pagina, legales };
}

@ -7,7 +7,6 @@
let { data } = $props();
const pagina = $derived(data.pagina);
const Cuerpo = $derived(data.Cuerpo);
const migas = $derived([{ texto: 'Legal', href: resolve('/legal') }, { texto: pagina.titulo }]);
</script>
@ -48,10 +47,15 @@
</p>
</header>
<!--
El cuerpo llega compuesto del servidor, desde la versión en vigor del
documento. Es contenido propio y de un solo autor; cuando exista la
administración habrá que sanearlo, y el sitio para hacerlo es
`$lib/server/markdown`, en un punto y no en cada plantilla.
-->
<article class="texto">
{#if Cuerpo}
<Cuerpo />
{/if}
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html pagina.html}
</article>
</div>
@ -60,12 +64,12 @@
consulta más que se lee: se entra a mirar el desistimiento o quién trata
los datos, no a leerlo de arriba abajo.
-->
{#if data.indice.length > 1}
{#if pagina.apartados.length > 1}
<aside class="apartados">
<nav aria-label="Apartados de esta página">
<p class="rotulo" id="lista-apartados">En esta página</p>
<ul aria-labelledby="lista-apartados">
{#each data.indice as apartado (apartado.ancla)}
{#each pagina.apartados as apartado (apartado.ancla)}
<li><a href="#{apartado.ancla}">{apartado.texto}</a></li>
{/each}
</ul>

@ -1,14 +0,0 @@
import { error } from '@sveltejs/kit';
import { cuerpoLegal, indiceLegal, legales, obtenerLegal } from '$lib/content';
export function load({ params }) {
const pagina = obtenerLegal(params.slug);
if (!pagina) error(404, 'Esa página no existe.');
return {
pagina,
legales,
Cuerpo: cuerpoLegal(params.slug),
indice: indiceLegal(params.slug)
};
}

@ -1,14 +1,7 @@
import {
canciones,
entradas,
estilos,
etiquetas,
legales,
paginas,
slugsDeAlbum
} from '$lib/content';
import { canciones, entradas, estilos, etiquetas, paginas, slugsDeAlbum } from '$lib/content';
import { site } from '$lib/site';
import { indiceDelTaller } from '$lib/server/taller';
import { indiceLegal } from '$lib/server/legal';
import type { RequestHandler } from './$types';
/**
@ -39,12 +32,11 @@ function url(ruta: string, prioridad: string, fecha?: string): string {
export const GET: RequestHandler = async () => {
/*
* El taller ya no sale de los archivos sino de la base, así que este
* controlador es asíncrono. Lo demás sigue en `src/content` hasta que le
* toque: un mapa a medias no es peor que uno viejo, pero uno que se olvide
* de los géneros nuevos sí.
* El taller y los textos legales ya no salen de los archivos sino de la
* base, así que este controlador es asíncrono. El catálogo sigue en
* `src/content` hasta que le toque.
*/
const { principios, fichas } = await indiceDelTaller();
const [{ principios, fichas }, legales] = await Promise.all([indiceDelTaller(), indiceLegal()]);
const rutas = [
url('/', PRIORIDAD.portada),

Loading…
Cancel
Save

Powered by TurnKey Linux.