Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
import { sep } from 'node:path';
|
|
|
|
|
import { mdsvex } from 'mdsvex';
|
|
|
|
|
import remarkBreaks from 'remark-breaks';
|
|
|
|
|
import { defineConfig } from 'vitest/config';
|
|
|
|
|
import { playwright } from '@vitest/browser-playwright';
|
|
|
|
|
import adapter from '@sveltejs/adapter-node';
|
|
|
|
|
import { sveltekit } from '@sveltejs/kit/vite';
|
|
|
|
|
import type { Plugin } from 'vite';
|
|
|
|
|
import type { PreprocessorGroup } from 'svelte/compiler';
|
|
|
|
|
import { conExtracto } from './src/lib/content/extracto.ts';
|
|
|
|
|
import { ancla } from './src/lib/content/anclas.ts';
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sirve el panel de contenido en /admin durante el desarrollo.
|
|
|
|
|
*
|
|
|
|
|
* En produccion lo resuelve el servidor de estaticos, que ya busca el
|
|
|
|
|
* index.html de una carpeta; el servidor de desarrollo de Vite no lo hace y
|
|
|
|
|
* /admin daria 404. Sin esto, la direccion del panel seria distinta en cada
|
|
|
|
|
* entorno, que es justo la clase de detalle que se olvida y hace perder media
|
|
|
|
|
* hora.
|
|
|
|
|
*/
|
|
|
|
|
const panelEnDesarrollo: Plugin = {
|
|
|
|
|
name: 'panel-en-desarrollo',
|
|
|
|
|
apply: 'serve',
|
|
|
|
|
configureServer(servidor) {
|
|
|
|
|
servidor.middlewares.use((peticion, _respuesta, siguiente) => {
|
|
|
|
|
const ruta = (peticion.url ?? '').split('?')[0];
|
|
|
|
|
if (ruta === '/admin' || ruta === '/admin/') peticion.url = '/admin/index.html';
|
|
|
|
|
siguiente();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Escribe en el frontmatter de cada cancion los primeros versos de su letra.
|
|
|
|
|
*
|
|
|
|
|
* Tiene que correr ANTES que mdsvex: despues, el cuerpo ya es un componente
|
|
|
|
|
* compilado y el texto ha dejado de ser texto. Con esto, las tarjetas pueden
|
|
|
|
|
* ensenar unos versos sin que nadie mantenga a mano un campo de resumen.
|
|
|
|
|
*/
|
|
|
|
|
const inyectarExtracto: PreprocessorGroup = {
|
|
|
|
|
name: 'extracto-de-cancion',
|
|
|
|
|
markup({ content, filename }) {
|
|
|
|
|
// En Windows los nombres llegan con barras invertidas.
|
|
|
|
|
if (!filename?.split(sep).join('/').includes('/src/content/canciones/')) return;
|
|
|
|
|
const conFragmento = conExtracto(content);
|
|
|
|
|
return conFragmento === content ? undefined : { code: conFragmento };
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Pone un `id` a cada encabezado de nivel 2 del Markdown.
|
|
|
|
|
*
|
|
|
|
|
* Hace falta para que el indice lateral de los textos largos pueda enlazar a
|
|
|
|
|
* sus apartados. Se escribe a mano en vez de traer `rehype-slug` porque son
|
|
|
|
|
* quince lineas, y porque el ancla tiene que salir de la misma funcion que usa
|
|
|
|
|
* el cargador al construir el indice: con dos implementaciones parecidas, un
|
|
|
|
|
* dia dejan de coincidir.
|
|
|
|
|
*/
|
|
|
|
|
type NodoHast = {
|
|
|
|
|
type: string;
|
|
|
|
|
tagName?: string;
|
|
|
|
|
value?: string;
|
|
|
|
|
properties?: Record<string, unknown>;
|
|
|
|
|
children?: NodoHast[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function textoDe(nodo: NodoHast): string {
|
|
|
|
|
if (nodo.type === 'text') return nodo.value ?? '';
|
|
|
|
|
return (nodo.children ?? []).map(textoDe).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function anclasEnEncabezados() {
|
|
|
|
|
return (arbol: NodoHast) => {
|
|
|
|
|
const recorrer = (nodo: NodoHast) => {
|
|
|
|
|
if (nodo.tagName === 'h2') {
|
|
|
|
|
nodo.properties = { ...nodo.properties, id: ancla(textoDe(nodo)) };
|
|
|
|
|
}
|
|
|
|
|
for (const hijo of nodo.children ?? []) recorrer(hijo);
|
|
|
|
|
};
|
|
|
|
|
recorrer(arbol);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
/**
|
|
|
|
|
* mdsvex 0.12 emite `<script context="module">` para exportar el frontmatter,
|
|
|
|
|
* sintaxis que Svelte 5 marca como obsoleta. El aviso apunta a los .md de
|
|
|
|
|
* contenido, donde no hay nada que corregir, y sale uno por archivo en cada
|
|
|
|
|
* compilacion.
|
|
|
|
|
*
|
|
|
|
|
* Se corrige la salida en lugar de silenciar el aviso: `sveltekit()` ignora
|
|
|
|
|
* `onwarn` cuando la configuracion viaja en el propio vite.config.ts.
|
|
|
|
|
*/
|
|
|
|
|
const corregirScriptDeMdsvex: PreprocessorGroup = {
|
|
|
|
|
name: 'mdsvex-script-module',
|
|
|
|
|
markup({ content, filename }) {
|
|
|
|
|
if (!filename?.endsWith('.md') && !filename?.endsWith('.svx')) return;
|
|
|
|
|
if (!content.includes('<script context="module">')) return;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
code: content.replaceAll('<script context="module">', '<script module>')
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default defineConfig({
|
|
|
|
|
plugins: [
|
|
|
|
|
panelEnDesarrollo,
|
|
|
|
|
sveltekit({
|
|
|
|
|
compilerOptions: {
|
|
|
|
|
// Force runes mode for the project, except for libraries. Can be removed in svelte 6.
|
|
|
|
|
runes: ({ filename }) =>
|
|
|
|
|
filename.split(/[/\\]/).includes('node_modules') ? undefined : true
|
|
|
|
|
},
|
|
|
|
|
adapter: adapter(),
|
|
|
|
|
preprocess: [
|
|
|
|
|
inyectarExtracto,
|
|
|
|
|
mdsvex({
|
|
|
|
|
extensions: ['.svx', '.md'],
|
|
|
|
|
// Sin remark-breaks, Markdown junta los versos de una letra en un
|
|
|
|
|
// unico parrafo: en una cancion el salto de linea es contenido.
|
|
|
|
|
// mdsvex declara los plugins con una version antigua de los tipos de
|
|
|
|
|
// unified, incompatible con la de remark-breaks 4. En ejecucion
|
|
|
|
|
// encajan; lo que choca es solo la firma.
|
|
|
|
|
// @ts-expect-error tipos de unified desalineados entre mdsvex y remark-breaks
|
|
|
|
|
remarkPlugins: [remarkBreaks],
|
|
|
|
|
rehypePlugins: [anclasEnEncabezados]
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
}),
|
|
|
|
|
corregirScriptDeMdsvex
|
|
|
|
|
],
|
|
|
|
|
extensions: ['.svelte', '.svx', '.md'],
|
|
|
|
|
typescript: {
|
|
|
|
|
config: (config) => {
|
|
|
|
|
config.include.push('../drizzle.config.ts');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
],
|
|
|
|
|
test: {
|
|
|
|
|
expect: { requireAssertions: true },
|
|
|
|
|
projects: [
|
|
|
|
|
{
|
|
|
|
|
extends: './vite.config.ts',
|
|
|
|
|
test: {
|
|
|
|
|
name: 'client',
|
|
|
|
|
browser: {
|
|
|
|
|
enabled: true,
|
|
|
|
|
provider: playwright(),
|
|
|
|
|
instances: [{ browser: 'chromium', headless: true }]
|
|
|
|
|
},
|
|
|
|
|
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
|
|
|
|
|
exclude: ['src/lib/server/**']
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
extends: './vite.config.ts',
|
|
|
|
|
test: {
|
|
|
|
|
name: 'server',
|
|
|
|
|
environment: 'node',
|
|
|
|
|
// Tambien las herramientas de `scripts/`: colocan archivos de audio
|
|
|
|
|
// segun una convencion de nombres, y equivocarla no da ningun error.
|
|
|
|
|
include: ['src/**/*.{test,spec}.{js,ts}', 'scripts/**/*.spec.mjs'],
|
|
|
|
|
exclude: ['src/**/*.svelte.{test,spec}.{js,ts}']
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
});
|