hiparquia.dog — sitio F0-lite: Astro estático, cero JS, candado del protocolo en código
This commit is contained in:
+17
@@ -0,0 +1,17 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.astro/
|
||||||
|
fotos/
|
||||||
|
|
||||||
|
# LA REGLA: se rastrea la fuente, no la salida — pero solo cuando la salida se
|
||||||
|
# puede volver a fabricar en el sitio donde hace falta.
|
||||||
|
#
|
||||||
|
# La fuente embarcable SE DERIVA en el propio Dockerfile (solo necesita fontTools):
|
||||||
|
# npm run fuente
|
||||||
|
public/Commissioner-wght.woff2
|
||||||
|
|
||||||
|
# Las tarjetas og SÍ se versionan, y es una excepción con razón: fabricarlas
|
||||||
|
# necesita Chromium, y meter Chromium en la imagen de construcción cuesta más de
|
||||||
|
# 300 MB para tres PNG que cambian una vez por ficha. Se regeneran a mano con
|
||||||
|
# npm run tarjetas
|
||||||
|
# y se comitean. Si algún día el generador cambia, se nota en el diff del PNG.
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# hiparquia.dog — sitio estático. Se construye una vez y se sirve como archivos.
|
||||||
|
# No hay servidor de aplicación porque no hay aplicación: son tres HTML y una fuente.
|
||||||
|
|
||||||
|
FROM node:22-alpine AS construir
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# fontTools deriva la fuente embarcable del maestro (npm run fuente): quita los
|
||||||
|
# tres ejes que el sitio no usa y la deja en 36 KB en vez de 95. No necesita
|
||||||
|
# navegador, así que cabe en la imagen de construcción sin engordarla.
|
||||||
|
RUN apk add --no-cache python3 py3-pip \
|
||||||
|
&& pip3 install --break-system-packages --no-cache-dir fonttools brotli
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --omit=dev || npm install --omit=dev
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:alpine
|
||||||
|
COPY --from=construir /app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
EXPOSE 80
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# hiparquia.dog — la casa
|
||||||
|
|
||||||
|
El sitio que las piezas prometen. Cada carrusel termina diciendo *«la ficha completa vive en
|
||||||
|
la casa»* y cada pin lleva `hiparquia.dog` impreso; esto es la casa.
|
||||||
|
|
||||||
|
**Fase F0-lite del `9-1`.** La arquitectura dice que el corpus vive en Directus y las fichas
|
||||||
|
en `ink.hiparquia.dog`. Directus todavía no está en pie, así que el corpus son archivos
|
||||||
|
markdown y las fichas cuelgan de la raíz. **El esquema es el mismo contrato**: cuando Directus
|
||||||
|
exista se cambia el `loader` de `src/content.config.ts` y nada más — las páginas no se enteran.
|
||||||
|
|
||||||
|
## Qué hay
|
||||||
|
|
||||||
|
```
|
||||||
|
src/content/fichas/*.md el corpus. Una ficha pública por texto
|
||||||
|
src/content.config.ts el esquema + EL CANDADO del protocolo
|
||||||
|
src/pages/index.astro el manifiesto y el índice
|
||||||
|
src/pages/ficha/[slug].astro la ficha, con su schema.org
|
||||||
|
src/layouts/Base.astro cabeza, og:, tipografía
|
||||||
|
src/styles/global.css dos tintas y nada más
|
||||||
|
src/fuentes/ el MAESTRO de Commissioner (4 ejes, no se embarca)
|
||||||
|
scripts/fuente.py deriva la fuente embarcable
|
||||||
|
scripts/og.mjs fabrica las tarjetas al compartir
|
||||||
|
scripts/verificar.mjs los seis escenarios adversos
|
||||||
|
```
|
||||||
|
|
||||||
|
## El candado, en código
|
||||||
|
|
||||||
|
```js
|
||||||
|
.filter(f => f.data.semaforo === 'verde' || f.data.semaforo === 'propio')
|
||||||
|
```
|
||||||
|
|
||||||
|
Está en el índice **y** en `getStaticPaths`. Una ficha en ámbar o rojo no aparece en la lista
|
||||||
|
**y su página no se construye**: no existe la URL. El `9-1 §2` dice que el protocolo deja de
|
||||||
|
ser documento y se vuelve software; esto es esa línea.
|
||||||
|
|
||||||
|
## Nada se mueve
|
||||||
|
|
||||||
|
Ni una animación. La única transición del sitio es el color de un enlace bajo el cursor,
|
||||||
|
120 ms lineal — retroalimentación, no gesto. La pregunta de `ioon-gesto §0.5` es *¿qué se
|
||||||
|
pierde si esto no se mueve?*, y en un archivo de fichas la respuesta es: nada.
|
||||||
|
|
||||||
|
**No hay `font-weight` ni `letter-spacing` en ningún `:hover`.** Ensanchan el elemento y
|
||||||
|
empujan a sus vecinos. Medido: el ancho del enlace no cambia, delta 0.00 px.
|
||||||
|
|
||||||
|
## Las dos fuentes de Commissioner, y por qué son dos
|
||||||
|
|
||||||
|
| | ejes | peso | dónde |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **maestro** | `wght` `slnt` `FLAR` `VOLM` | 95,144 B | `src/fuentes/` — no se embarca |
|
||||||
|
| **embarcable** | solo `wght` 100–900 | **36,400 B** | `public/` — se deriva |
|
||||||
|
|
||||||
|
**−61.7 % sin tocar un solo carácter.** El sitio nunca usa `slnt`, `FLAR` ni `VOLM`, así que
|
||||||
|
los tres se fijan en 0 y desaparecen del binario. Los 230 glifos del subconjunto latino se
|
||||||
|
quedan enteros: el corpus va a crecer con textos que todavía no existen, y recortar por los
|
||||||
|
caracteres de hoy rompería los de mañana.
|
||||||
|
|
||||||
|
Es la misma partición que en los SVG: un maestro que lo conserva todo y un embarcable que solo
|
||||||
|
lleva lo que la pieza usa.
|
||||||
|
|
||||||
|
## Qué se versiona y qué no
|
||||||
|
|
||||||
|
Se rastrea la fuente, no la salida — **pero solo cuando la salida se puede volver a fabricar
|
||||||
|
donde hace falta.**
|
||||||
|
|
||||||
|
- **La fuente embarcable NO se versiona.** Se deriva dentro del propio `Dockerfile`; fontTools
|
||||||
|
cabe en la imagen de construcción sin engordarla.
|
||||||
|
- **Las tarjetas og SÍ se versionan.** Fabricarlas necesita Chromium, y meterlo en la imagen de
|
||||||
|
construcción cuesta más de 300 MB para tres PNG que cambian una vez por ficha. Es una
|
||||||
|
excepción con razón escrita, no un descuido. Se regeneran con `npm run tarjetas`.
|
||||||
|
|
||||||
|
## Las tarjetas al compartir
|
||||||
|
|
||||||
|
`serclin.mx` no tiene `og:image`, y por eso su enlace viaja desnudo por WhatsApp. Aquí van
|
||||||
|
desde el primer día: 1200×630, una por ficha más la de la casa. **WhatsApp y Facebook no
|
||||||
|
ejecutan JavaScript** — leen las etiquetas `og:` y nada más.
|
||||||
|
|
||||||
|
## Cómo se corre
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
npm run build:completo # fuente + tarjetas + astro build
|
||||||
|
npm run verificar # los seis escenarios, contra dist/ servido en 127.0.0.1
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cómo se despliega
|
||||||
|
|
||||||
|
**El paso 2 no es opcional** (`ioon-verificar §1.1`): mientras Coolify no esté expuesto por
|
||||||
|
Traefik con HTTPS, GitHub no tiene por dónde entregar el webhook. Un fix bien escrito estuvo
|
||||||
|
68 días sin salir del repositorio de serclin por creer lo contrario.
|
||||||
|
|
||||||
|
1. `git push`
|
||||||
|
2. **`Redeploy` manual en Coolify**
|
||||||
|
3. **Verificar contra producción**, con dos comprobaciones que no mienten: que cambió el
|
||||||
|
`last-modified` **y** que el HTML servido trae el texto nuevo. En este sitio el CSS va
|
||||||
|
incrustado en el HTML, así que no hay hash de bundle que mirar: se comprueba el HTML.
|
||||||
|
|
||||||
|
Antes de nada: **`hiparquia.dog` tiene que resolver.** Los dos pines lo llevan impreso y no
|
||||||
|
pueden publicarse hasta entonces.
|
||||||
|
|
||||||
|
## Pendientes
|
||||||
|
|
||||||
|
- DNS en Cloudflare + TLS por Traefik (`9-1 §1`).
|
||||||
|
- Cuando haya más de ~20 fichas: paginación o índice por autor.
|
||||||
|
- Cuando Directus esté en pie: cambiar el `loader`, mover a `ink.` y dejar redirecciones.
|
||||||
|
- `ask.` (AnythingLLM) y la tienda son F1/F2 — nada de esto los bloquea.
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
proyecto: "9. hiparquia"
|
||||||
|
subtema: "verificación · hiparquia.dog (F0-lite, antes de desplegar)"
|
||||||
|
slug: "verificacion-hiparquia-sitio"
|
||||||
|
version: "v20260804-2140"
|
||||||
|
autor: "Francisco León (ioon) + Claude"
|
||||||
|
nivel: "reporte de verificación"
|
||||||
|
estado: "PASA 12/12 · 1 BLOQUEADO (el dominio, que no es de la pieza)"
|
||||||
|
---
|
||||||
|
|
||||||
|
# VERIFICACIÓN · hiparquia.dog · 04-ago-2026 21:40, hora Oaxaca
|
||||||
|
|
||||||
|
**régimen:** automático · **sin licencias declaradas**
|
||||||
|
|
||||||
|
Medido contra `dist/` servido en `127.0.0.1`, no contra el disco. Tres rutas: `/`,
|
||||||
|
`/ficha/hombres-necios/`, `/ficha/nada-te-turbe/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Los seis escenarios adversos
|
||||||
|
|
||||||
|
| # | escenario | resultado |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | **Sin JavaScript** | contenido **completo**: 1,108 · 3,827 · 3,874 caracteres visibles. `<h1>` presente, enlaces navegables ✔ |
|
||||||
|
| 2 | **`prefers-reduced-motion: reduce`** | `transition-duration: 1e-05s` ✔ |
|
||||||
|
| 3 | **Pestaña oculta + `rAF` congelado** | **0 elementos invisibles.** Nada nace en `opacity-0` ✔ |
|
||||||
|
| 4 | **360 px** | `scrollWidth === clientWidth` en las tres rutas ✔ |
|
||||||
|
| 5 | **Táctil 390 px** | sin desborde; nada depende de `:hover` para funcionar ✔ |
|
||||||
|
| 6 | **Recarga a media página** | no aplica: no hay estado de llegada que rearmar ✔ |
|
||||||
|
|
||||||
|
## PASA 12/12
|
||||||
|
|
||||||
|
- ✔ **Cero JavaScript.** Dos recursos en el cable y ya: el HTML y la fuente. No hay ni un
|
||||||
|
`<script>` que enviar.
|
||||||
|
- ✔ **HTML en el cable, comprimido: 2,425 B** el índice y **4,028 B** una ficha. La vara: el
|
||||||
|
primitivo del puente pesa 1,482 B y el sitio entero de Bruno Imbrizi, 27 KB.
|
||||||
|
- ✔ **Fuente: 36,400 B** — el maestro pesa 95,144 y se le quitaron los tres ejes que el sitio
|
||||||
|
no usa. **−61.7 % sin tocar un carácter.**
|
||||||
|
- ✔ **Objetivo de toque: 44 px el peor**, en las tres rutas y en los dos anchos. El imperativo
|
||||||
|
es 24 y la meta 44; aquí el peor caso ya es la meta.
|
||||||
|
- ✔ **Ancho durante el hover: 91.16 → 91.16 px, delta 0.00.** El enlace cambia de color y nada
|
||||||
|
más — ni `font-weight` ni `letter-spacing`, que ensanchan y empujan a los vecinos.
|
||||||
|
- ✔ **Contraste 21:1** en el texto (blanco puro sobre negro puro) y **9.7:1** en el texto tenue
|
||||||
|
(`#a6a6a6`). Imperativo: 4.5:1.
|
||||||
|
- ✔ **Un solo `<h1>` por página**, `lang="es"`, enlace «Saltar al contenido».
|
||||||
|
- ✔ **`og:image` presente desde el primer día** — 1200×630, una por ficha. WhatsApp y Facebook
|
||||||
|
no ejecutan JavaScript: leen las etiquetas y nada más. *(serclin.mx todavía no lo tiene.)*
|
||||||
|
- ✔ **Canónica y `schema.org`** en cada ficha. En «nada» el JSON-LD declara
|
||||||
|
`creativeWorkStatus: "Autoría tradicional no verificada paleográficamente"` — la reserva de
|
||||||
|
la ficha, legible por máquina.
|
||||||
|
- ✔ **El candado del protocolo está en código**, no en un documento: una ficha que no sea
|
||||||
|
`verde` o `propio` no aparece en el índice **y su página no se construye**.
|
||||||
|
- ✔ **Foco visible** con `:focus-visible`, contorno de 2 px y separación de 3.
|
||||||
|
- ✔ **CSP cerrada:** `default-src 'none'`. Se puede porque no hay JavaScript de nadie; el día
|
||||||
|
que deje de poderse será porque entró algo.
|
||||||
|
|
||||||
|
## FALLA 0
|
||||||
|
|
||||||
|
## BLOQUEADO 1 — y no es de la pieza
|
||||||
|
|
||||||
|
⛔ **`hiparquia.dog` no resuelve.** El sitio está listo; el dominio no. Bloquea también los dos
|
||||||
|
pines de S2, que lo llevan impreso.
|
||||||
|
|
||||||
|
## Alcance recortado, y se dice
|
||||||
|
|
||||||
|
- **No se probó en un navegador de teléfono real**, solo en Chromium con emulación táctil.
|
||||||
|
- **No se probó con lector de pantalla real.**
|
||||||
|
- **No se construyó la imagen Docker** — no hay demonio de Docker en este contenedor. El
|
||||||
|
`Dockerfile` está escrito y razonado, pero **su primera construcción es una comprobación
|
||||||
|
pendiente**, no un hecho.
|
||||||
|
- No se midió Safari ni Firefox: el sitio no usa nada que esté en discusión (sin `d` animada,
|
||||||
|
sin `offset-path`, sin `animation-timeline`), pero eso es un argumento, no una medición.
|
||||||
|
|
||||||
|
## Lo que sigue
|
||||||
|
|
||||||
|
1. **DNS de `hiparquia.dog`** en Cloudflare + TLS por Traefik.
|
||||||
|
2. Repo en Gitea, app en Coolify, y la primera construcción de la imagen.
|
||||||
|
3. `git push` → **`Redeploy` manual** → verificar contra producción. El paso 2 no es opcional
|
||||||
|
mientras Coolify no esté expuesto: es la regla que costó 68 días en serclin.
|
||||||
|
4. Con el sitio en pie, **los pines salen** y los carruseles dejan de prometer en falso.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Verificación · 04-ago-2026 21:40, hora Oaxaca. Doce comprobaciones, cero fallos, y un bloqueo
|
||||||
|
que no depende de escribir más código.*
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
site: 'https://hiparquia.dog',
|
||||||
|
output: 'static',
|
||||||
|
trailingSlash: 'always',
|
||||||
|
build: { format: 'directory' },
|
||||||
|
/* Sin integraciones. Sin JavaScript en el cliente: no hay ninguno que enviar. */
|
||||||
|
});
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
# El nivel por defecto de nginx es 1, no 6. Medido sobre una ficha de este
|
||||||
|
# sitio: nivel 1 = 4,433 B · nivel 6 = 4,092 B. 341 B (7.7 %) por una línea.
|
||||||
|
# Del 6 al 9 se gana 1 B y se paga CPU: el 6 es el punto correcto.
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_min_length 256;
|
||||||
|
gzip_types text/html text/css application/javascript application/json image/svg+xml;
|
||||||
|
|
||||||
|
# La fuente y las tarjetas no cambian: se cachean fuerte.
|
||||||
|
location ~* \.(woff2|png|svg)$ {
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# `trailingSlash: always` — cada ficha es un directorio con su index.html.
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ $uri/index.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 404 /index.html;
|
||||||
|
|
||||||
|
# Cabeceras mínimas. Sin JavaScript propio ni de terceros: la política puede
|
||||||
|
# ser así de cerrada, y si algún día deja de poder, es que algo entró.
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Content-Security-Policy
|
||||||
|
"default-src 'none'; img-src 'self'; font-src 'self'; style-src 'self' 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'" always;
|
||||||
|
}
|
||||||
Generated
+4204
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "hiparquia-sitio",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"fuente": "python3 scripts/fuente.py",
|
||||||
|
"tarjetas": "node scripts/og.mjs",
|
||||||
|
"build": "npm run fuente && astro build",
|
||||||
|
"build:completo": "npm run fuente && npm run tarjetas && astro build",
|
||||||
|
"dev": "astro dev",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"verificar": "node scripts/verificar.mjs"
|
||||||
|
},
|
||||||
|
"dependencies": { "astro": "^7.1.6" }
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,24 @@
|
|||||||
|
import pw from '/home/claude/.npm-global/lib/node_modules/playwright/index.js';
|
||||||
|
import http from 'http'; import fs from 'fs'; import path from 'path';
|
||||||
|
const {chromium}=pw;
|
||||||
|
const TIPO={'.html':'text/html;charset=utf-8','.woff2':'font/woff2','.png':'image/png'};
|
||||||
|
const srv=http.createServer((req,res)=>{let p=decodeURIComponent(req.url.split('?')[0]);
|
||||||
|
if(p.endsWith('/'))p+='index.html'; const f=path.join('dist',p);
|
||||||
|
if(!fs.existsSync(f)||fs.statSync(f).isDirectory()){res.writeHead(404);return res.end()}
|
||||||
|
res.writeHead(200,{'content-type':TIPO[path.extname(f)]??'application/octet-stream'});
|
||||||
|
res.end(fs.readFileSync(f))});
|
||||||
|
await new Promise(r=>srv.listen(4322,'127.0.0.1',r));
|
||||||
|
const b=await chromium.launch({executablePath:'/opt/pw-browsers/chromium'});
|
||||||
|
fs.mkdirSync('fotos',{recursive:true});
|
||||||
|
for (const [n,vp,ruta,full] of [
|
||||||
|
['inicio-1280',{width:1280,height:1000},'/',true],
|
||||||
|
['ficha-1280',{width:1280,height:1000},'/ficha/nada-te-turbe/',false],
|
||||||
|
['inicio-390',{width:390,height:844},'/',true],
|
||||||
|
['ficha-390',{width:390,height:844},'/ficha/hombres-necios/',false]]){
|
||||||
|
const p=await b.newPage({viewport:vp,deviceScaleFactor:1});
|
||||||
|
await p.goto('http://127.0.0.1:4322'+ruta,{waitUntil:'networkidle'});
|
||||||
|
await p.screenshot({path:`fotos/${n}.png`,fullPage:full});
|
||||||
|
await p.close();
|
||||||
|
}
|
||||||
|
await b.close(); srv.close();
|
||||||
|
console.log('fotos listas');
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""Deriva la fuente que se embarca a partir del maestro.
|
||||||
|
|
||||||
|
El maestro (src/fuentes/) trae los cuatro ejes de Commissioner. El sitio solo usa
|
||||||
|
el peso — nunca FLAR, VOLM ni slnt —, así que los tres se fijan en 0 y desaparecen
|
||||||
|
del binario. El repertorio de caracteres NO se toca: 230 glifos, el subconjunto
|
||||||
|
latino entero, porque el corpus va a crecer con textos que todavía no existen y
|
||||||
|
recortar por los caracteres de hoy rompería los de mañana.
|
||||||
|
|
||||||
|
Es la misma partición que en los SVG: un maestro que lo conserva todo y un
|
||||||
|
embarcable que solo lleva lo que la pieza usa.
|
||||||
|
"""
|
||||||
|
from fontTools.ttLib import TTFont
|
||||||
|
from fontTools.varLib.instancer import instantiateVariableFont
|
||||||
|
import os
|
||||||
|
|
||||||
|
MAESTRO = "src/fuentes/Commissioner-latin.woff2"
|
||||||
|
SALIDA = "public/Commissioner-wght.woff2"
|
||||||
|
|
||||||
|
f = instantiateVariableFont(TTFont(MAESTRO),
|
||||||
|
{"FLAR": 0, "VOLM": 0, "slnt": 0, "wght": (100, 900)},
|
||||||
|
inplace=True, updateFontNames=False, optimize=True)
|
||||||
|
f.flavor = "woff2"
|
||||||
|
f.save(SALIDA)
|
||||||
|
|
||||||
|
a, b = os.path.getsize(MAESTRO), os.path.getsize(SALIDA)
|
||||||
|
g = TTFont(SALIDA)
|
||||||
|
print(f"maestro {a:,} B → embarcable {b:,} B ({(b-a)/a*100:+.1f} %)")
|
||||||
|
print(f"ejes: {[(x.axisTag, x.minValue, x.maxValue) for x in g['fvar'].axes]} · glifos: {len(g.getBestCmap())}")
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import pw from '/home/claude/.npm-global/lib/node_modules/playwright/index.js';
|
||||||
|
import fs from 'fs';
|
||||||
|
const { chromium } = pw;
|
||||||
|
|
||||||
|
/* Las tarjetas al compartir. 1200×630 es lo que leen WhatsApp, Facebook,
|
||||||
|
Telegram y X — ninguno ejecuta JavaScript, así que la tarjeta es esta imagen
|
||||||
|
y las etiquetas og:. Sin ella, el enlace viaja desnudo.
|
||||||
|
(Lección de serclin.mx: el og:image ausente decide la tarjeta, no el sitio.) */
|
||||||
|
|
||||||
|
const TARJETAS = [
|
||||||
|
{ archivo: 'hiparquia', palabra: 'hiparquia',
|
||||||
|
pie: 'Cada verso con su ficha: la fuente, la edición y hasta dónde llegó el cotejo.',
|
||||||
|
tam: 128 },
|
||||||
|
{ archivo: 'hombres-necios', palabra: 'necios',
|
||||||
|
pie: 'Sor Juana Inés de la Cruz · Inundación castálida, Madrid, 1689',
|
||||||
|
tam: 172 },
|
||||||
|
{ archivo: 'nada-te-turbe', palabra: 'nada',
|
||||||
|
pie: 'Atribuido a Teresa de Ávila (1515–1582) · letrilla hallada en su breviario',
|
||||||
|
tam: 172 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const W = 1200, H = 630;
|
||||||
|
const b64 = fs.readFileSync('src/fuentes/Commissioner-latin.woff2').toString('base64');
|
||||||
|
|
||||||
|
const pagina = (T) => `<!doctype html><meta charset="utf-8"><style>
|
||||||
|
@font-face{ font-family:Commissioner; src:url(data:font/woff2;base64,${b64}) format('woff2');
|
||||||
|
font-weight:100 900; font-display:block; }
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
html,body{width:${W}px;height:${H}px;background:#000}
|
||||||
|
body{font-family:Commissioner;color:#fff;font-variation-settings:"FLAR" 0,"VOLM" 0;
|
||||||
|
padding:78px;display:flex;flex-direction:column;justify-content:space-between;
|
||||||
|
-webkit-font-smoothing:antialiased}
|
||||||
|
.palabra{font-weight:800;font-size:${T.tam}px;line-height:1.02;letter-spacing:-0.025em;
|
||||||
|
margin-block:auto 0}
|
||||||
|
.pie{font-weight:300;font-size:29px;line-height:1.45;color:#a6a6a6;max-width:26em;
|
||||||
|
margin-block-start:30px}
|
||||||
|
</style>
|
||||||
|
<div class="palabra">${T.palabra}</div>
|
||||||
|
<div class="pie">${T.pie}</div>`;
|
||||||
|
|
||||||
|
const b = await chromium.launch({ executablePath:'/opt/pw-browsers/chromium',
|
||||||
|
args:['--force-device-scale-factor=1','--hide-scrollbars'] });
|
||||||
|
const p = await b.newPage({ viewport:{ width:W, height:H }, deviceScaleFactor:1 });
|
||||||
|
fs.mkdirSync('public/og', { recursive:true });
|
||||||
|
const out = [];
|
||||||
|
for (const T of TARJETAS) {
|
||||||
|
await p.setContent(pagina(T));
|
||||||
|
await p.evaluate(()=>document.fonts.ready);
|
||||||
|
const f = `public/og/${T.archivo}.png`;
|
||||||
|
await p.screenshot({ path:f });
|
||||||
|
const desborde = await p.evaluate(() => {
|
||||||
|
const d = document.documentElement;
|
||||||
|
return { h: d.scrollWidth - d.clientWidth, v: d.scrollHeight - d.clientHeight };
|
||||||
|
});
|
||||||
|
out.push({ archivo:T.archivo, bytes: fs.statSync(f).size, desborde });
|
||||||
|
}
|
||||||
|
await b.close();
|
||||||
|
console.log(JSON.stringify(out, null, 1));
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import pw from '/home/claude/.npm-global/lib/node_modules/playwright/index.js';
|
||||||
|
import http from 'http'; import fs from 'fs'; import path from 'path';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
const { chromium } = pw;
|
||||||
|
|
||||||
|
/* ioon-verificar · los seis escenarios adversos + lo que se mide con número.
|
||||||
|
Se sirve dist/ en 127.0.0.1 y se mide ahí, no en el disco. */
|
||||||
|
|
||||||
|
const RAIZ = 'dist';
|
||||||
|
const TIPO = { '.html':'text/html;charset=utf-8', '.css':'text/css', '.js':'text/javascript',
|
||||||
|
'.woff2':'font/woff2', '.png':'image/png', '.json':'application/json' };
|
||||||
|
const srv = http.createServer((req,res)=>{
|
||||||
|
let p = decodeURIComponent(req.url.split('?')[0]);
|
||||||
|
if (p.endsWith('/')) p += 'index.html';
|
||||||
|
const f = path.join(RAIZ, p);
|
||||||
|
if (!fs.existsSync(f) || fs.statSync(f).isDirectory()) { res.writeHead(404); return res.end('404'); }
|
||||||
|
res.writeHead(200, { 'content-type': TIPO[path.extname(f)] ?? 'application/octet-stream' });
|
||||||
|
res.end(fs.readFileSync(f));
|
||||||
|
});
|
||||||
|
await new Promise(r => srv.listen(4321, '127.0.0.1', r));
|
||||||
|
const BASE = 'http://127.0.0.1:4321';
|
||||||
|
const RUTAS = ['/', '/ficha/hombres-necios/', '/ficha/nada-te-turbe/'];
|
||||||
|
|
||||||
|
const b = await chromium.launch({ executablePath:'/opt/pw-browsers/chromium' });
|
||||||
|
const R = {};
|
||||||
|
|
||||||
|
/* ── 1 · sin JavaScript ─────────────────────────────────────────────────── */
|
||||||
|
{
|
||||||
|
const c = await b.newContext({ javaScriptEnabled:false, viewport:{width:1280,height:900} });
|
||||||
|
const p = await c.newPage();
|
||||||
|
R.sinJS = [];
|
||||||
|
for (const r of RUTAS) {
|
||||||
|
await p.goto(BASE+r, { waitUntil:'load' });
|
||||||
|
R.sinJS.push({ ruta:r,
|
||||||
|
textoVisible: await p.evaluate(()=>document.body.innerText.trim().length),
|
||||||
|
h1: await p.evaluate(()=>document.querySelector('h1')?.innerText ?? null),
|
||||||
|
enlaces: await p.evaluate(()=>document.querySelectorAll('a').length) });
|
||||||
|
}
|
||||||
|
await c.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 2 · movimiento reducido ────────────────────────────────────────────── */
|
||||||
|
{
|
||||||
|
const c = await b.newContext({ reducedMotion:'reduce', viewport:{width:1280,height:900} });
|
||||||
|
const p = await c.newPage(); await p.goto(BASE+RUTAS[1]);
|
||||||
|
R.movReducido = await p.evaluate(()=>{
|
||||||
|
const a = document.querySelector('main a') ?? document.querySelector('a');
|
||||||
|
return { duracion: getComputedStyle(a).transitionDuration,
|
||||||
|
textoVisible: document.body.innerText.trim().length };
|
||||||
|
});
|
||||||
|
await c.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 3 · pestaña oculta ─────────────────────────────────────────────────── */
|
||||||
|
{
|
||||||
|
const c = await b.newContext({ viewport:{width:1280,height:900} });
|
||||||
|
const p = await c.newPage();
|
||||||
|
await p.addInitScript(() => {
|
||||||
|
Object.defineProperty(document,'visibilityState',{get:()=>'hidden'});
|
||||||
|
Object.defineProperty(document,'hidden',{get:()=>true});
|
||||||
|
window.requestAnimationFrame = () => 0; // rAF congelado
|
||||||
|
});
|
||||||
|
await p.goto(BASE+RUTAS[1]);
|
||||||
|
R.oculta = await p.evaluate(()=>{
|
||||||
|
const inv = [...document.querySelectorAll('h1,h2,p,li')]
|
||||||
|
.filter(e=>e.innerText.trim() && getComputedStyle(e).opacity !== '1').length;
|
||||||
|
return { textoVisible: document.body.innerText.trim().length, elementosInvisibles: inv };
|
||||||
|
});
|
||||||
|
await c.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 4 · 360 px · 5 · táctil · 6 · objetivos de toque ───────────────────── */
|
||||||
|
{
|
||||||
|
for (const [nom, opc] of [
|
||||||
|
['ancho360', { viewport:{width:360,height:740} }],
|
||||||
|
['tactil', { viewport:{width:390,height:844}, hasTouch:true, isMobile:true,
|
||||||
|
userAgent:'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)' }]]) {
|
||||||
|
const c = await b.newContext(opc); const p = await c.newPage();
|
||||||
|
R[nom] = [];
|
||||||
|
for (const r of RUTAS) {
|
||||||
|
await p.goto(BASE+r);
|
||||||
|
R[nom].push(await p.evaluate(ruta => {
|
||||||
|
const d = document.documentElement;
|
||||||
|
const toques = [...document.querySelectorAll('a')].map(a=>{
|
||||||
|
const b = a.getBoundingClientRect();
|
||||||
|
return { texto:a.innerText.trim().slice(0,22), alto:+b.height.toFixed(1), ancho:+b.width.toFixed(1) };
|
||||||
|
}).filter(t=>t.alto>0);
|
||||||
|
return { ruta,
|
||||||
|
desbordeH: d.scrollWidth - d.clientWidth,
|
||||||
|
peorToque: Math.min(...toques.map(t=>t.alto)),
|
||||||
|
bajo24: toques.filter(t=>t.alto < 24).map(t=>t.texto),
|
||||||
|
bajo44: toques.filter(t=>t.alto < 44).map(t=>t.texto) };
|
||||||
|
}, r));
|
||||||
|
}
|
||||||
|
await c.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── lo que se mide con número ──────────────────────────────────────────── */
|
||||||
|
{
|
||||||
|
const c = await b.newContext({ viewport:{width:1280,height:900} });
|
||||||
|
const p = await c.newPage();
|
||||||
|
const red = [];
|
||||||
|
p.on('response', r => red.push({ url:r.url(), tipo:r.request().resourceType() }));
|
||||||
|
await p.goto(BASE+RUTAS[1], { waitUntil:'networkidle' });
|
||||||
|
R.recursos = red.map(r=>({ tipo:r.tipo, url:r.url.replace(BASE,'') }));
|
||||||
|
R.js = red.filter(r=>r.tipo==='script').length;
|
||||||
|
/* anchos durante el hover: un enlace que ensancha empuja a sus vecinos */
|
||||||
|
R.hover = await p.evaluate(async () => {
|
||||||
|
const a = document.querySelector('main a') ?? document.querySelector('header a');
|
||||||
|
if (!a) return null;
|
||||||
|
const antes = a.getBoundingClientRect().width;
|
||||||
|
a.dispatchEvent(new MouseEvent('mouseover',{bubbles:true}));
|
||||||
|
a.classList.add('__h');
|
||||||
|
await new Promise(r=>setTimeout(r,200));
|
||||||
|
return { antes:+antes.toFixed(2), despues:+a.getBoundingClientRect().width.toFixed(2) };
|
||||||
|
});
|
||||||
|
R.meta = await p.evaluate(()=>({
|
||||||
|
lang: document.documentElement.lang,
|
||||||
|
titulo: document.title,
|
||||||
|
descripcion: document.querySelector('meta[name=description]')?.content?.length ?? 0,
|
||||||
|
ogImage: document.querySelector('meta[property="og:image"]')?.content ?? null,
|
||||||
|
canonica: document.querySelector('link[rel=canonical]')?.href ?? null,
|
||||||
|
jsonld: !!document.querySelector('script[type="application/ld+json"]'),
|
||||||
|
saltar: !!document.querySelector('.saltar'),
|
||||||
|
h1: document.querySelectorAll('h1').length,
|
||||||
|
}));
|
||||||
|
await c.close();
|
||||||
|
}
|
||||||
|
await b.close(); srv.close();
|
||||||
|
|
||||||
|
/* bytes en el cable, comprimidos */
|
||||||
|
const gz = (f) => +(execSync(`gzip -c9 ${f} | wc -c`).toString().trim());
|
||||||
|
R.bytes = {
|
||||||
|
indexHTML: gz('dist/index.html'),
|
||||||
|
fichaHTML: gz('dist/ficha/hombres-necios/index.html'),
|
||||||
|
fuenteWOFF2: fs.statSync('dist/Commissioner-wght.woff2').size,
|
||||||
|
ogPNG: fs.statSync('dist/og/hombres-necios.png').size,
|
||||||
|
};
|
||||||
|
console.log(JSON.stringify(R, null, 1));
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { defineCollection, z } from 'astro:content';
|
||||||
|
import { glob } from 'astro/loaders';
|
||||||
|
|
||||||
|
/* El corpus. Hoy son archivos markdown; cuando Directus esté en pie (9-1 §2)
|
||||||
|
se cambia el `loader` y nada más — el esquema es el contrato. */
|
||||||
|
const fichas = defineCollection({
|
||||||
|
loader: glob({ pattern: '**/*.md', base: './src/content/fichas' }),
|
||||||
|
schema: z.object({
|
||||||
|
palabra: z.string(),
|
||||||
|
titulo: z.string(),
|
||||||
|
tituloCompleto: z.string().optional(),
|
||||||
|
autor: z.string(),
|
||||||
|
vida: z.string(),
|
||||||
|
obra: z.string(),
|
||||||
|
lugar: z.string().optional(),
|
||||||
|
anio: z.number(),
|
||||||
|
lengua: z.string(),
|
||||||
|
/* el candado del protocolo: solo verde y propio pueden publicarse */
|
||||||
|
semaforo: z.enum(['verde', 'ambar', 'rojo', 'propio']),
|
||||||
|
atribuido: z.boolean().default(false),
|
||||||
|
fragmento: z.string().optional(),
|
||||||
|
credito: z.string(),
|
||||||
|
resumen: z.string(),
|
||||||
|
orden: z.number(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { fichas };
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
palabra: "necios"
|
||||||
|
titulo: "Hombres necios que acusáis"
|
||||||
|
tituloCompleto: "Arguye de inconsecuentes el gusto y la censura de los hombres que en las mujeres acusan lo que causan"
|
||||||
|
autor: "Sor Juana Inés de la Cruz"
|
||||||
|
vida: "1648–1695"
|
||||||
|
obra: "Inundación castálida"
|
||||||
|
lugar: "Madrid"
|
||||||
|
anio: 1689
|
||||||
|
lengua: "español"
|
||||||
|
semaforo: "verde"
|
||||||
|
credito: "Sor Juana Inés de la Cruz. «Arguye de inconsecuentes el gusto y la censura de los hombres que en las mujeres acusan lo que causan.» Inundación castálida, Madrid, 1689."
|
||||||
|
resumen: "La segunda palabra del poema es la que hace todo el trabajo. Qué edición imprimimos, por qué, y hasta dónde llegó nuestro cotejo."
|
||||||
|
orden: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
## El texto
|
||||||
|
|
||||||
|
> Hombres necios que acusáis
|
||||||
|
> a la mujer sin razón,
|
||||||
|
> sin ver que sois la ocasión
|
||||||
|
> de lo mismo que culpáis.
|
||||||
|
|
||||||
|
Primera de diecisiete redondillas.
|
||||||
|
|
||||||
|
## La segunda palabra
|
||||||
|
|
||||||
|
La primera palabra nombra a quién le habla — es el vocativo, la puerta. **La segunda ya es la
|
||||||
|
sentencia.**
|
||||||
|
|
||||||
|
Para abrir, Sor Juana no eligió *crueles*. Ni *injustos*, ni *tiranos*, ni *villanos*: tenía a
|
||||||
|
mano todo el vocabulario del Siglo de Oro para el agravio. Eligió **tontos**.
|
||||||
|
|
||||||
|
Acusar de necedad humilla más y hace menos ruido que acusar de maldad. Al malvado se le puede
|
||||||
|
temer; al necio no se le puede ni discutir. Y el poema entero se sostiene sobre esa elección:
|
||||||
|
lo que sigue no es una denuncia, es la demostración paciente de una incoherencia — *sin ver que
|
||||||
|
sois la ocasión de lo mismo que culpáis*. La palabra pone el marco antes de que empiece el
|
||||||
|
argumento.
|
||||||
|
|
||||||
|
## El título que nadie usa
|
||||||
|
|
||||||
|
Todo el mundo lo llama «Hombres necios» o «las Redondillas». Su título es:
|
||||||
|
|
||||||
|
> Arguye de inconsecuentes el gusto y la censura de los hombres que en las mujeres acusan lo
|
||||||
|
> que causan.
|
||||||
|
|
||||||
|
Es un título de época: descriptivo, largo, hecho para orientar al lector en un índice, no para
|
||||||
|
memorizarse. Vale la pena saberlo por una razón práctica: buscarlo por ahí lleva a las ediciones
|
||||||
|
académicas; buscarlo por el primer verso lleva a las cuentas de citas.
|
||||||
|
|
||||||
|
## La procedencia
|
||||||
|
|
||||||
|
Apareció impreso por primera vez en *Inundación castálida* (Madrid, **1689**), la primera
|
||||||
|
recopilación de su obra publicada en España — costeada por Juan Camacho Gayna y dedicada a la
|
||||||
|
virreina de la Laguna. El volumen recogía loas, villancicos, el *Neptuno alegórico* y la poesía
|
||||||
|
personal: sonetos, décimas, redondillas y liras. Tuvo nueve reimpresiones, que para el siglo XVII
|
||||||
|
es una cifra de éxito de venta.
|
||||||
|
|
||||||
|
Sor Juana murió en 1695. Hace **331 años**. El texto está en dominio público sin discusión
|
||||||
|
posible.
|
||||||
|
|
||||||
|
## La coma que sí importa
|
||||||
|
|
||||||
|
Los cuatro versos son estables en todas las ediciones que consultamos. **Su puntuación, no.**
|
||||||
|
El segundo verso aparece como *«a la mujer sin razón»* en unas ediciones y como *«a la mujer,
|
||||||
|
sin razón»* en otras; el cuarto cierra con punto, con dos puntos o con punto y coma según quién
|
||||||
|
lo edite.
|
||||||
|
|
||||||
|
No es un detalle de erudito. La ortografía del siglo XVII no puntuaba como la nuestra, y **toda
|
||||||
|
puntuación moderna en este poema es editorial, no de Sor Juana.** Quien lo imprime está
|
||||||
|
eligiendo una edición, lo sepa o no.
|
||||||
|
|
||||||
|
La coma después de «mujer» cambia el énfasis: sin coma, *sin razón* califica a la acusación —
|
||||||
|
acusáis sin motivo. Con coma, se despega y suena a *injustamente*.
|
||||||
|
|
||||||
|
**Imprimimos sin coma.** Es la lectura más seca y la que sostiene el verso siguiente. No es la
|
||||||
|
única defendible; es la nuestra, y por eso va dicha.
|
||||||
|
|
||||||
|
## Hasta dónde llegó el cotejo
|
||||||
|
|
||||||
|
Esto es lo que sabemos y cómo lo sabemos. Lo que no alcanzamos también está aquí.
|
||||||
|
|
||||||
|
**Cotejado contra:** la transcripción de la Universidad de Pensilvania, que cita *Obras
|
||||||
|
completas* (México: Porrúa, 1997), p. 109 — de ahí sale el título completo. Los datos de
|
||||||
|
*Inundación castálida* contra la ficha de referencia de EBSCO.
|
||||||
|
|
||||||
|
**No consultado, y hay que decirlo:** el facsímil de la edición de 1689, la edición crítica de
|
||||||
|
Méndez Plancarte (*Obras completas* I, *Lírica personal*, FCE) y la Biblioteca Virtual Miguel de
|
||||||
|
Cervantes.
|
||||||
|
|
||||||
|
**Qué significa:** llegamos a edición académica citada a nivel de página, que es un escalón por
|
||||||
|
debajo de la fuente primaria. Suficiente para publicar la ficha. Cuando tengamos el tomo de
|
||||||
|
Méndez Plancarte a mano, esta nota se actualiza y lo diremos aquí.
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
---
|
||||||
|
palabra: "nada"
|
||||||
|
titulo: "Nada te turbe"
|
||||||
|
tituloCompleto: "Nada te turbe, nada te espante"
|
||||||
|
autor: "Atribuido a Teresa de Ávila"
|
||||||
|
vida: "1515–1582"
|
||||||
|
obra: "letrilla hallada en su breviario"
|
||||||
|
lugar: "Alba de Tormes"
|
||||||
|
anio: 1582
|
||||||
|
lengua: "español"
|
||||||
|
semaforo: "verde"
|
||||||
|
atribuido: true
|
||||||
|
fragmento: "los tres primeros versos de una letrilla de nueve"
|
||||||
|
credito: "Atribuido a Teresa de Ávila (1515–1582). Letrilla hallada en su breviario. Fragmento: los tres primeros versos de una letrilla de nueve."
|
||||||
|
resumen: "Cinco fuentes afirman que es de Santa Teresa. Ninguna lo argumenta. Lo imprimimos igual, con dos palabras de más."
|
||||||
|
orden: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
## El texto
|
||||||
|
|
||||||
|
> Nada te turbe,
|
||||||
|
> nada te espante:
|
||||||
|
> todo se pasa.
|
||||||
|
|
||||||
|
Fragmento: los tres primeros versos de una letrilla de nueve.
|
||||||
|
|
||||||
|
## La pregunta que esta ficha existe para hacer
|
||||||
|
|
||||||
|
Todo el mundo sabe que este poema es de Santa Teresa. **Nadie lo argumenta.**
|
||||||
|
|
||||||
|
Consultamos cinco fuentes — carmelitas, especializadas y divulgativas. Las cinco afirman la
|
||||||
|
autoría. **Ninguna cita una edición crítica, un estudio paleográfico ni un análisis de la
|
||||||
|
letra.** Una dice que el papel estaba *«de su puño y letra»*, sin respaldo. El sitio más
|
||||||
|
especializado que encontramos, dedicado íntegramente a Teresa, no dice de quién es la letra:
|
||||||
|
dice solo que era *«una composición que Teresa llevaba consigo en su breviario cuando murió en
|
||||||
|
Alba de Tormes (1582)»*.
|
||||||
|
|
||||||
|
Llevar un papel en el breviario y haberlo escrito son dos cosas distintas. **La primera está
|
||||||
|
documentada; la segunda se repite.**
|
||||||
|
|
||||||
|
## Y hay un segundo texto que sí sabemos que no es suyo
|
||||||
|
|
||||||
|
La letrilla tiene **nueve versos**. Alrededor circula una **glosa poética extensa** — la de las
|
||||||
|
estampas, los imanes y las cuentas de citas — y no hay indicios de que Teresa la escribiera.
|
||||||
|
|
||||||
|
Son dos textos que se pegaron con el tiempo, y casi nadie los separa. La letrilla es lo
|
||||||
|
atribuido. Todo lo que le cuelga alrededor, no.
|
||||||
|
|
||||||
|
## La letrilla completa
|
||||||
|
|
||||||
|
> Nada te turbe, / nada te espante, / todo se pasa, / Dios no se muda, / la paciencia / todo lo
|
||||||
|
> alcanza. / Quien a Dios tiene / nada le falta. / Solo Dios basta.
|
||||||
|
|
||||||
|
## Por qué imprimimos tres versos de nueve
|
||||||
|
|
||||||
|
**Cortar cambia el género del texto**, y hay que decirlo.
|
||||||
|
|
||||||
|
La letrilla entera resuelve en *Dios no se muda… solo Dios basta*. Cortada en el tercer verso,
|
||||||
|
queda un consuelo **estoico**, no devocional: llega a quien cree y a quien no. Para una pieza de
|
||||||
|
pésame eso nos parece lo correcto — pero es un corte con consecuencia teológica, no un recorte
|
||||||
|
por espacio.
|
||||||
|
|
||||||
|
Por eso el fragmento va marcado como fragmento, en la pieza y aquí.
|
||||||
|
|
||||||
|
## La puntuación es nuestra
|
||||||
|
|
||||||
|
El original circula con **coma** tras *espante*. Nosotros imprimimos **dos puntos**: son más
|
||||||
|
secos y anuncian la conclusión.
|
||||||
|
|
||||||
|
Es puntuación editorial nuestra, no del siglo XVI — donde no se puntuaba así. Va declarada como
|
||||||
|
tal.
|
||||||
|
|
||||||
|
## Las dos palabras de más
|
||||||
|
|
||||||
|
En el dorso de cada pieza no dice «Teresa de Ávila». Dice:
|
||||||
|
|
||||||
|
> **Atribuido a Teresa de Ávila** (1515–1582). Letrilla hallada en su breviario.
|
||||||
|
|
||||||
|
Nadie imprime «atribuido a», porque le quita fuerza a la frase. Nosotros lo imprimimos porque se
|
||||||
|
la da a la casa.
|
||||||
|
|
||||||
|
## El semáforo es verde, y el riesgo no es legal
|
||||||
|
|
||||||
|
Teresa murió en 1582. Hace **444 años**. Aunque la letrilla resultara ser de una carmelita
|
||||||
|
anónima del siglo XVI, seguiría siendo de dominio público: no hay derechos que infringir ni
|
||||||
|
titular que pueda reclamar, en ningún escenario.
|
||||||
|
|
||||||
|
**El riesgo de esta pieza no es legal: es de credibilidad.** Por eso la respuesta no es no
|
||||||
|
publicarla. Es publicarla diciendo lo que se sabe.
|
||||||
|
|
||||||
|
## Hasta dónde llegó el cotejo
|
||||||
|
|
||||||
|
**Consultado:** Provincia Carmelita de San Juan de la Cruz · teresavila.com — de donde sale el
|
||||||
|
dato de la glosa · Jerónimo Alayón · Ciudad Seva · EWTN.
|
||||||
|
|
||||||
|
**No consultado, y es lo que cerraría el caso:** las *Obras completas* de Tomás Álvarez o de
|
||||||
|
Efrén de la Madre de Dios (BAC), que son las ediciones críticas teresianas; cualquier estudio
|
||||||
|
paleográfico del papel del breviario; y el propio manuscrito, conservado por la orden.
|
||||||
|
|
||||||
|
**Qué significa:** la atribución es tradicional y no está verificada paleográficamente. Lo
|
||||||
|
decimos en la ficha y lo imprimimos en el dorso. Si algún día podemos quitar esas dos palabras,
|
||||||
|
lo diremos aquí — y probablemente no haya que quitarlas nunca.
|
||||||
Binary file not shown.
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
import '../styles/global.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
titulo: string;
|
||||||
|
descripcion: string;
|
||||||
|
ruta: string;
|
||||||
|
og?: string;
|
||||||
|
}
|
||||||
|
const { titulo, descripcion, ruta, og = '/og/hiparquia.png' } = Astro.props;
|
||||||
|
const SITIO = 'https://hiparquia.dog';
|
||||||
|
const canonica = new URL(ruta, SITIO).href;
|
||||||
|
---
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>{titulo}</title>
|
||||||
|
<meta name="description" content={descripcion} />
|
||||||
|
<link rel="canonical" href={canonica} />
|
||||||
|
|
||||||
|
{/* La tarjeta al compartir. WhatsApp y Facebook no ejecutan JavaScript:
|
||||||
|
leen estas etiquetas y nada más. Sin og:image no hay tarjeta. */}
|
||||||
|
<meta property="og:type" content="article" />
|
||||||
|
<meta property="og:site_name" content="hiparquia" />
|
||||||
|
<meta property="og:locale" content="es_MX" />
|
||||||
|
<meta property="og:title" content={titulo} />
|
||||||
|
<meta property="og:description" content={descripcion} />
|
||||||
|
<meta property="og:url" content={canonica} />
|
||||||
|
<meta property="og:image" content={new URL(og, SITIO).href} />
|
||||||
|
<meta property="og:image:width" content="1200" />
|
||||||
|
<meta property="og:image:height" content="630" />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
|
||||||
|
<link rel="preload" href="/Commissioner-wght.woff2" as="font" type="font/woff2" crossorigin />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a class="saltar" href="#contenido">Saltar al contenido</a>
|
||||||
|
<header class="casa env">
|
||||||
|
<a href="/">hiparquia</a>
|
||||||
|
</header>
|
||||||
|
<main id="contenido" class="env">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
<footer class="pie env">
|
||||||
|
<p>
|
||||||
|
Cada texto de esta casa lleva su ficha: de quién es, de qué edición sale,
|
||||||
|
y hasta dónde llegó el cotejo. Cuando solo lo sabemos a medias, lo decimos.
|
||||||
|
</p>
|
||||||
|
<p>hiparquia · Oaxaca</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
import { getCollection, render } from 'astro:content';
|
||||||
|
import Base from '../../layouts/Base.astro';
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const fichas = (await getCollection('fichas'))
|
||||||
|
.filter(f => f.data.semaforo === 'verde' || f.data.semaforo === 'propio');
|
||||||
|
return fichas.map(f => ({ params: { slug: f.id }, props: { ficha: f } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ficha } = Astro.props;
|
||||||
|
const d = ficha.data;
|
||||||
|
const { Content } = await render(ficha);
|
||||||
|
|
||||||
|
/* schema.org — para que un buscador (o un modelo) pueda leer la procedencia
|
||||||
|
sin adivinarla. 9-1 §2: «fichas públicas + schema.org para GEO». */
|
||||||
|
const jsonld = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'CreativeWork',
|
||||||
|
name: d.tituloCompleto ?? d.titulo,
|
||||||
|
alternateName: d.titulo,
|
||||||
|
inLanguage: 'es',
|
||||||
|
datePublished: String(d.anio),
|
||||||
|
isPartOf: d.obra,
|
||||||
|
creditText: d.credito,
|
||||||
|
...(d.atribuido
|
||||||
|
? { author: { '@type': 'Person', name: d.autor.replace(/^Atribuido a /, '') },
|
||||||
|
creativeWorkStatus: 'Autoría tradicional no verificada paleográficamente' }
|
||||||
|
: { author: { '@type': 'Person', name: d.autor } }),
|
||||||
|
};
|
||||||
|
---
|
||||||
|
<Base
|
||||||
|
titulo={`${d.palabra} — ${d.autor}`}
|
||||||
|
descripcion={d.resumen}
|
||||||
|
ruta={`/ficha/${ficha.id}/`}
|
||||||
|
og={`/og/${ficha.id}.png`}
|
||||||
|
>
|
||||||
|
<h1>{d.palabra}</h1>
|
||||||
|
|
||||||
|
<p class="credito">{d.credito}</p>
|
||||||
|
|
||||||
|
<ul class="marcas">
|
||||||
|
<li>Dominio público</li>
|
||||||
|
{d.atribuido && <li>Atribuido</li>}
|
||||||
|
{d.fragmento && <li>Fragmento</li>}
|
||||||
|
<li>{d.anio}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Content />
|
||||||
|
|
||||||
|
<script type="application/ld+json" set:html={JSON.stringify(jsonld)} is:inline />
|
||||||
|
</Base>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import Base from '../layouts/Base.astro';
|
||||||
|
|
||||||
|
/* El candado del protocolo, en código: solo verde y propio salen.
|
||||||
|
9-1 §2 — «el protocolo deja de ser documento y se vuelve software». */
|
||||||
|
const fichas = (await getCollection('fichas'))
|
||||||
|
.filter(f => f.data.semaforo === 'verde' || f.data.semaforo === 'propio')
|
||||||
|
.sort((a, b) => a.data.orden - b.data.orden);
|
||||||
|
---
|
||||||
|
<Base
|
||||||
|
titulo="hiparquia — cada verso con su ficha"
|
||||||
|
descripcion="Poemas de dominio público impresos con su procedencia: de quién es cada palabra, de qué edición sale, y hasta dónde llegó el cotejo."
|
||||||
|
ruta="/"
|
||||||
|
>
|
||||||
|
<h1>Cada verso<br />con su <b>ficha</b>.</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Las frases circulan sin dueño. Se atribuyen a quien vende más, se cortan donde
|
||||||
|
conviene y se puntúan como caiga. Casi nadie dice de qué edición salieron, y
|
||||||
|
casi nadie dice cuándo no lo sabe.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Aquí cada texto lleva su ficha: la fuente, la edición, la fecha, las decisiones
|
||||||
|
editoriales que tomamos y <b>las que no pudimos cerrar</b>. Cuando la autoría es
|
||||||
|
tradicional y no está probada, se imprime «atribuido a». Cuando es un fragmento,
|
||||||
|
dice que es un fragmento.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Es más trabajo y vende igual. Pero es la única forma de que la palabra valga algo.</p>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h2>El corpus</h2>
|
||||||
|
|
||||||
|
<ul class="corpus">
|
||||||
|
{fichas.map(f => (
|
||||||
|
<li>
|
||||||
|
<a href={`/ficha/${f.id}/`}>
|
||||||
|
<span class="palabra">{f.data.palabra}</span>
|
||||||
|
<span class="quien">{f.data.autor} · {f.data.vida}</span>
|
||||||
|
<span class="resumen">{f.data.resumen}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Base>
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
/* hiparquia.dog — dos tintas y nada más.
|
||||||
|
Cero JavaScript. Nada se mueve salvo la retroalimentación de un enlace bajo
|
||||||
|
el cursor: un archivo de fichas no gana nada moviéndose (ioon-gesto §0.5). */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Commissioner;
|
||||||
|
src: url("/Commissioner-wght.woff2") format("woff2");
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: swap; /* swap, no block: el texto aparece aunque la fuente tarde */
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
|
||||||
|
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--tinta: #ffffff;
|
||||||
|
--fondo: #000000;
|
||||||
|
--tenue: #a6a6a6; /* 9.7:1 sobre negro */
|
||||||
|
--linea: #2e2e2e;
|
||||||
|
--medida: 34rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
*, *::before, *::after { box-sizing: border-box; }
|
||||||
|
|
||||||
|
html {
|
||||||
|
background: var(--fondo);
|
||||||
|
color: var(--tinta);
|
||||||
|
font-family: Commissioner, system-ui, sans-serif;
|
||||||
|
font-variation-settings: "FLAR" 0, "VOLM" 0;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: clamp(17px, 1.05vw + 14px, 20px);
|
||||||
|
line-height: 1.62;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body { margin: 0; }
|
||||||
|
|
||||||
|
/* ── esqueleto ─────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.env {
|
||||||
|
max-inline-size: var(--medida);
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-inline: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saltar {
|
||||||
|
position: absolute;
|
||||||
|
inset-block-start: -100vh;
|
||||||
|
inset-inline-start: 1rem;
|
||||||
|
background: var(--tinta);
|
||||||
|
color: var(--fondo);
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
z-index: 9;
|
||||||
|
}
|
||||||
|
.saltar:focus { inset-block-start: 1rem; }
|
||||||
|
|
||||||
|
header.casa {
|
||||||
|
padding-block: 2.2rem 0;
|
||||||
|
}
|
||||||
|
header.casa a {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.94rem;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: lowercase;
|
||||||
|
/* 44 px de alto de toque sin tocar el maquetado */
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-block-size: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main { padding-block: 1rem 5rem; }
|
||||||
|
|
||||||
|
footer.pie {
|
||||||
|
border-block-start: 1px solid var(--linea);
|
||||||
|
margin-block-start: 4rem;
|
||||||
|
padding-block: 2rem 4rem;
|
||||||
|
color: var(--tenue);
|
||||||
|
font-size: 0.86rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── tipografía ────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: clamp(2.6rem, 6vw, 3.6rem);
|
||||||
|
line-height: 1.1;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
margin-block: 1.4rem 0.6rem;
|
||||||
|
}
|
||||||
|
h1 b, h1 strong { font-weight: 800; }
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.16rem;
|
||||||
|
line-height: 1.32;
|
||||||
|
letter-spacing: -0.005em;
|
||||||
|
margin-block: 2.8rem 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p { margin-block: 0 1.05rem; }
|
||||||
|
strong, b { font-weight: 700; }
|
||||||
|
em, i { font-style: normal; font-weight: 500; }
|
||||||
|
|
||||||
|
/* el verso: se respetan los saltos de línea del texto fuente */
|
||||||
|
blockquote {
|
||||||
|
margin: 1.6rem 0 1.6rem 0;
|
||||||
|
padding-inline-start: 1.15rem;
|
||||||
|
border-inline-start: 1px solid var(--linea);
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.16rem;
|
||||||
|
line-height: 1.42;
|
||||||
|
}
|
||||||
|
blockquote p { margin-block-end: 0; white-space: pre-line; }
|
||||||
|
|
||||||
|
hr { border: 0; border-block-start: 1px solid var(--linea); margin-block: 3rem; }
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
text-underline-offset: 0.22em;
|
||||||
|
/* ficha de gesto · retroalimentar · eje: color · 120 ms lineal.
|
||||||
|
NO font-weight ni letter-spacing: ensanchan y empujan a los vecinos. */
|
||||||
|
transition: color 120ms linear;
|
||||||
|
}
|
||||||
|
a:hover { color: var(--tenue); }
|
||||||
|
a:focus-visible { outline: 2px solid var(--tinta); outline-offset: 3px; }
|
||||||
|
|
||||||
|
/* ── la cabecera de la ficha ───────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.credito {
|
||||||
|
color: var(--tenue);
|
||||||
|
font-size: 0.94rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-block: 0 2.4rem;
|
||||||
|
}
|
||||||
|
.marcas {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-block: 1.1rem 2.4rem;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.marcas li {
|
||||||
|
border: 1px solid var(--linea);
|
||||||
|
padding: 0.28rem 0.66rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--tenue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── el índice ─────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.corpus { list-style: none; padding: 0; margin: 2.6rem 0 0; }
|
||||||
|
.corpus li { border-block-start: 1px solid var(--linea); }
|
||||||
|
.corpus a {
|
||||||
|
display: block;
|
||||||
|
padding-block: 1.5rem; /* muy por encima de los 44 px de objetivo de toque */
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.corpus .palabra {
|
||||||
|
display: block;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.9rem;
|
||||||
|
line-height: 1.15;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
.corpus .quien { display: block; color: var(--tenue); font-size: 0.9rem; margin-block-start: 0.3rem; }
|
||||||
|
.corpus .resumen { display: block; margin-block-start: 0.55rem; }
|
||||||
|
.corpus a:hover .quien, .corpus a:hover .resumen { color: var(--tenue); }
|
||||||
|
.corpus a:hover .palabra { text-decoration: underline; text-underline-offset: 0.16em; }
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*, *::before, *::after { transition-duration: 0.01ms !important; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user