hiparquia.dog — sitio F0-lite: Astro estático, cero JS, candado del protocolo en código
This commit is contained in:
@@ -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));
|
||||
Reference in New Issue
Block a user