Adds gzip compression and ETag support for better caching

Implements response compression and ETag headers to optimize asset delivery and enable client-side caching, reducing bandwidth usage and improving load times. Enhances code block styling and syntax highlighting for documentation clarity.

Adds gzip compression, ETag caching, and code block styling

Improves server performance by implementing gzip response compression
and ETag-based caching for static and dynamic content, reducing
bandwidth and enabling efficient client-side caching. Enhances
documentation clarity with improved code block styling and
syntax highlighting support.
This commit is contained in:
2025-10-08 10:08:09 -05:00
parent b0198c7092
commit d561fede88
4 changed files with 219 additions and 22 deletions

View File

@@ -23,6 +23,29 @@ body {
animation: glow 3s ease-in-out infinite; animation: glow 3s ease-in-out infinite;
} }
/* Code blocks enhancements */
.code-block pre {
background: transparent !important;
margin: 0;
padding: 0;
}
.code-block pre code {
display: block;
padding: 1rem 1.25rem;
font-size: 0.9rem;
line-height: 1.5;
}
/* Etiquetas visuales API/CLI (apoyadas por header dinámico) */
.code-block .is-api {
background: linear-gradient(90deg, rgba(99,102,241,0.15), rgba(147,51,234,0.15));
}
.code-block .is-cli {
background: linear-gradient(90deg, rgba(236,72,153,0.15), rgba(59,130,246,0.15));
}
/* Scrollbar personalizado (opcional, moderno) */ /* Scrollbar personalizado (opcional, moderno) */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 8px;

View File

@@ -0,0 +1,86 @@
// Mejora de bloques de código: resaltar, barra de acciones (copiar), y etiquetas API/CLI
(function () {
function enhanceCodeBlocks() {
const pres = document.querySelectorAll('pre');
pres.forEach((pre) => {
if (pre.dataset.enhanced === '1') return;
pre.dataset.enhanced = '1';
// Asegurar que exista <code>
let codeEl = pre.querySelector('code');
const textRaw = pre.textContent || '';
if (!codeEl) {
codeEl = document.createElement('code');
codeEl.textContent = textRaw;
const t = textRaw.trimStart();
const isJSON = t.startsWith('{') || t.startsWith('[');
const isCLI = /^(!|\$|curl\s|#)/m.test(t);
if (isJSON) codeEl.className = 'language-json';
else if (isCLI) codeEl.className = 'language-bash';
else codeEl.className = 'language-text';
pre.textContent = '';
pre.appendChild(codeEl);
}
const wrapper = document.createElement('div');
wrapper.className = 'code-block group relative overflow-hidden rounded-xl border border-white/10 bg-slate-900/70 shadow-lg animate-[slideIn_0.4s_ease-out]';
const header = document.createElement('div');
header.className = 'flex items-center justify-between px-3 py-2 text-xs bg-slate-800/60 border-b border-white/10';
const label = document.createElement('span');
label.className = 'font-mono tracking-wide text-slate-300';
const lang = (codeEl.className || '').toLowerCase();
let kind = '';
if (/bash|shell|sh/.test(lang)) kind = 'CLI';
else if (/json|typescript|javascript/.test(lang)) kind = 'API';
label.textContent = kind || (lang.replace('language-', '').toUpperCase() || 'CODE');
if (kind === 'API') header.classList.add('is-api');
else if (kind === 'CLI') header.classList.add('is-cli');
const actions = document.createElement('div');
actions.className = 'flex items-center gap-1';
const copyBtn = document.createElement('button');
copyBtn.type = 'button';
copyBtn.className = 'rounded-md px-2 py-1 text-slate-300 hover:text-white hover:bg-white/10 transition';
copyBtn.textContent = 'Copiar';
copyBtn.addEventListener('click', async () => {
try {
const text = codeEl.innerText || pre.innerText || '';
await navigator.clipboard.writeText(text);
copyBtn.textContent = 'Copiado!';
setTimeout(() => (copyBtn.textContent = 'Copiar'), 1200);
} catch (err) {
// silencioso
}
});
header.appendChild(label);
header.appendChild(actions);
actions.appendChild(copyBtn);
const content = document.createElement('div');
content.className = 'relative';
if (pre.parentNode) pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(header);
wrapper.appendChild(content);
content.appendChild(pre);
pre.classList.add('overflow-auto');
});
if (window.hljs) {
document.querySelectorAll('pre code').forEach((el) => {
window.hljs.highlightElement(el);
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', enhanceCodeBlocks);
} else {
enhanceCodeBlocks();
}
})();

View File

@@ -1,11 +1,14 @@
import { createServer, IncomingMessage, ServerResponse } from "node:http"; import { createServer, IncomingMessage, ServerResponse } from "node:http";
import { promises as fs } from "node:fs"; import { promises as fs } from "node:fs";
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";
import { gzipSync } from "node:zlib";
import path from "node:path"; import path from "node:path";
import ejs from "ejs"; import ejs from "ejs";
const publicDir = path.join(__dirname, "public"); const publicDir = path.join(__dirname, "public");
const viewsDir = path.join(__dirname, "views"); const viewsDir = path.join(__dirname, "views");
// Compresión síncrona (rápida para tamaños pequeños de HTML/CSS/JS)
// Cargar metadatos del proyecto para usarlos como variables en las vistas // Cargar metadatos del proyecto para usarlos como variables en las vistas
let pkg: { let pkg: {
@@ -112,6 +115,29 @@ function applySecurityHeaders(base: Record<string, string> = {}) {
}; };
} }
function computeEtag(buf: Buffer): string {
// Weak ETag derived from content sha1 and length
const hash = createHash("sha1").update(buf).digest("base64");
return `W/"${buf.length.toString(16)}-${hash}"`;
}
function acceptsEncoding(req: IncomingMessage, enc: string): boolean {
const ae = (req.headers["accept-encoding"] as string) || "";
return ae
.split(",")
.map((s) => s.trim())
.includes(enc);
}
function shouldCompress(mime: string): boolean {
return (
mime.startsWith("text/") ||
mime.includes("json") ||
mime.includes("javascript") ||
mime.includes("svg")
);
}
const resolvePath = (pathname: string): string => { const resolvePath = (pathname: string): string => {
const decoded = decodeURIComponent(pathname); const decoded = decodeURIComponent(pathname);
let target = decoded; let target = decoded;
@@ -128,6 +154,7 @@ const resolvePath = (pathname: string): string => {
}; };
const sendResponse = async ( const sendResponse = async (
req: IncomingMessage,
res: ServerResponse, res: ServerResponse,
filePath: string, filePath: string,
statusCode = 200 statusCode = 200
@@ -138,18 +165,49 @@ const sendResponse = async (
? "no-cache" ? "no-cache"
: "public, max-age=86400, immutable"; : "public, max-age=86400, immutable";
const stat = await fs.stat(filePath).catch(() => undefined);
const data = await fs.readFile(filePath); const data = await fs.readFile(filePath);
res.writeHead( const etag = computeEtag(data);
statusCode,
applySecurityHeaders({ // Conditional requests
"Content-Type": mimeType, const inm = (req.headers["if-none-match"] as string) || "";
"Cache-Control": cacheControl, if (inm && inm === etag) {
}) res.writeHead(
); 304,
res.end(data); applySecurityHeaders({
ETag: etag,
"Cache-Control": cacheControl,
...(stat ? { "Last-Modified": stat.mtime.toUTCString() } : {}),
})
);
res.end();
return;
}
let body: any = data;
const headers: Record<string, string> = {
"Content-Type": mimeType,
"Cache-Control": cacheControl,
ETag: etag,
...(stat ? { "Last-Modified": stat.mtime.toUTCString() } : {}),
};
if (shouldCompress(mimeType) && acceptsEncoding(req, "gzip")) {
try {
body = gzipSync(data);
headers["Content-Encoding"] = "gzip";
headers["Vary"] = "Accept-Encoding";
} catch {
// Si falla compresión, enviar sin comprimir
}
}
res.writeHead(statusCode, applySecurityHeaders(headers));
res.end(body);
}; };
const renderTemplate = async ( const renderTemplate = async (
req: IncomingMessage,
res: ServerResponse, res: ServerResponse,
template: string, template: string,
locals: Record<string, any> = {}, locals: Record<string, any> = {},
@@ -157,7 +215,7 @@ const renderTemplate = async (
) => { ) => {
const pageFile = path.join(viewsDir, "pages", `${template}.ejs`); const pageFile = path.join(viewsDir, "pages", `${template}.ejs`);
const layoutFile = path.join(viewsDir, "layouts", "layout.ejs"); const layoutFile = path.join(viewsDir, "layouts", "layout.ejs");
const body = await ejs.renderFile(pageFile, locals, { async: true }); const pageBody = await ejs.renderFile(pageFile, locals, { async: true });
const defaultTitle = `${ const defaultTitle = `${
locals.appName ?? pkg.name ?? "Amayo Bot" locals.appName ?? pkg.name ?? "Amayo Bot"
} | Guía Completa`; } | Guía Completa`;
@@ -168,18 +226,43 @@ const renderTemplate = async (
scripts: null, scripts: null,
...locals, ...locals,
title: locals.title ?? defaultTitle, title: locals.title ?? defaultTitle,
body, body: pageBody,
}, },
{ async: true } { async: true }
); );
res.writeHead( const htmlBuffer = Buffer.from(html, "utf8");
statusCode, const etag = computeEtag(htmlBuffer);
applySecurityHeaders({
"Content-Type": "text/html; charset=utf-8", // Conditional ETag for dynamic page (fresh each deploy change)
"Cache-Control": "no-cache", const inm = (req.headers["if-none-match"] as string) || "";
}) if (inm && inm === etag) {
); res.writeHead(
res.end(html); 304,
applySecurityHeaders({ ETag: etag, "Cache-Control": "no-cache" })
);
res.end();
return;
}
let respBody: any = htmlBuffer;
const headers: Record<string, string> = {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-cache",
ETag: etag,
};
if (acceptsEncoding(req, "gzip")) {
try {
respBody = gzipSync(htmlBuffer);
headers["Content-Encoding"] = "gzip";
headers["Vary"] = "Accept-Encoding";
} catch {
// continuar sin comprimir
}
}
res.writeHead(statusCode, applySecurityHeaders(headers));
res.end(respBody);
}; };
export const server = createServer( export const server = createServer(
@@ -250,7 +333,7 @@ export const server = createServer(
year: "numeric", year: "numeric",
}); });
const djsVersion = pkg?.dependencies?.["discord.js"] ?? "15.0.0-dev"; const djsVersion = pkg?.dependencies?.["discord.js"] ?? "15.0.0-dev";
await renderTemplate(res, "index", { await renderTemplate(req, res, "index", {
appName: pkg.name ?? "Amayo Bot", appName: pkg.name ?? "Amayo Bot",
version: pkg.version ?? "2.0.0", version: pkg.version ?? "2.0.0",
djsVersion, djsVersion,
@@ -268,12 +351,12 @@ export const server = createServer(
} }
try { try {
await sendResponse(res, filePath); await sendResponse(req, res, filePath);
} catch (error: any) { } catch (error: any) {
if (error.code === "ENOENT") { if (error.code === "ENOENT") {
const notFoundPath = path.join(publicDir, "404.html"); const notFoundPath = path.join(publicDir, "404.html");
try { try {
await sendResponse(res, notFoundPath, 404); await sendResponse(req, res, notFoundPath, 404);
} catch { } catch {
res.writeHead( res.writeHead(
404, 404,
@@ -285,7 +368,7 @@ export const server = createServer(
} }
} else if (error.code === "EISDIR") { } else if (error.code === "EISDIR") {
const indexPath = path.join(filePath, "index.html"); const indexPath = path.join(filePath, "index.html");
await sendResponse(res, indexPath); await sendResponse(req, res, indexPath);
} else { } else {
console.error("[Server] Error al servir archivo:", error); console.error("[Server] Error al servir archivo:", error);
res.writeHead( res.writeHead(

View File

@@ -6,6 +6,10 @@
<title><%= title || `${appName} | Guía Completa` %></title> <title><%= title || `${appName} | Guía Completa` %></title>
<meta name="description" content="Guía completa de Amayo Bot: comandos, minijuegos, economía, misiones, logros, creación de contenido y más"> <meta name="description" content="Guía completa de Amayo Bot: comandos, minijuegos, economía, misiones, logros, creación de contenido y más">
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<!-- highlight.js (ligero y CDN) -->
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" integrity="sha512-6ZLqk2QG1lX7+K2bK2x2dFQ8rW1nKT2u2L7e9rM3gqcm9fKfH8dH9dfiR2x7kQ7E0m8b9lQYwA0mPt5aCq+gag==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" integrity="sha512-T7v6ZQ3+u7f0kZpJ8M9xgV4P0k1pHkq8oH9YfJjY0s2k9C1m1KcQq7z2d0aVj8pJm2Gd0v7C5y8Q4m1xQWJ8Gg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script> <script>
tailwind.config = { tailwind.config = {
theme: { theme: {
@@ -94,6 +98,7 @@
</footer> </footer>
<script src="/assets/js/main.js" type="module"></script> <script src="/assets/js/main.js" type="module"></script>
<script src="/assets/js/code.js" defer></script>
<% if (typeof scripts !== 'undefined' && scripts) { %> <% if (typeof scripts !== 'undefined' && scripts) { %>
<%= scripts %> <%= scripts %>
<% } %> <% } %>