Implementación completa del sitio JAILGAMES (bloques A–J)

Sitio estático generado con Eleventy (11ty) + Nginx en Docker:
- Plantillas Nunjucks con layout base, tarjetas y fichas individuales
- Datos de juegos en YAML, colección ordenada por fecha
- CSS con tema oscuro gaming y diseño responsive (3/2/1 columnas)
- Lightbox vanilla JS para capturas de pantalla
- Build multi-stage Docker (node:20-alpine → nginx:alpine)
- 2 juegos de ejemplo con imágenes SVG placeholder

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-30 12:58:55 +02:00
parent 93e41baf26
commit e337d7bc45
29 changed files with 3680 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
const yaml = require("js-yaml");
const fs = require("fs");
const path = require("path");
module.exports = function (eleventyConfig) {
// Parsear archivos YAML
eleventyConfig.addDataExtension("yaml,yml", (contents) =>
yaml.load(contents)
);
// Copiar archivos estáticos al build
// static/logo → _site/logo, static/games → _site/games
eleventyConfig.addPassthroughCopy({ "static/logo": "logo" });
eleventyConfig.addPassthroughCopy({ "static/games": "games" });
eleventyConfig.addPassthroughCopy("downloads");
eleventyConfig.addPassthroughCopy("src/css");
eleventyConfig.addPassthroughCopy("src/js");
// Colección: todos los juegos ordenados por fecha
eleventyConfig.addCollection("games", function () {
const gamesDir = path.join(__dirname, "src", "games");
const files = fs.readdirSync(gamesDir).filter((f) => f.endsWith(".yaml"));
return files
.map((f) => {
const data = yaml.load(
fs.readFileSync(path.join(gamesDir, f), "utf-8")
);
return data;
})
.sort((a, b) => new Date(b.release_date) - new Date(a.release_date));
});
// Filtro para formatear fechas en español
eleventyConfig.addFilter("dateFormat", function (dateStr) {
const d = new Date(dateStr + "T00:00:00");
return d.toLocaleDateString("es-ES", {
year: "numeric",
month: "long",
day: "numeric",
});
});
// Filtro Markdown para descripciones
const markdownIt = require("markdown-it");
const md = markdownIt({ html: true });
eleventyConfig.addFilter("markdown", (content) => md.render(content || ""));
return {
dir: {
input: "src",
includes: "_includes",
data: "_data",
output: "_site",
},
templateFormats: ["njk", "md"],
htmlTemplateEngine: "njk",
};
};
+14
View File
@@ -0,0 +1,14 @@
# Build output
_site/
# Node
node_modules/
# OS
.DS_Store
Thumbs.db
# Editor
.vscode/
.idea/
*.swp
+26
View File
@@ -0,0 +1,26 @@
# ---------- ETAPA 1: Build ----------
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY src/ src/
COPY static/ static/
COPY .eleventy.js ./
RUN npx @11ty/eleventy
# ---------- ETAPA 2: Servir ----------
FROM nginx:alpine
# Configuración personalizada de Nginx
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copiar el sitio generado
COPY --from=builder /app/_site /usr/share/nginx/html
# Copiar descargas (no pasan por Eleventy)
COPY downloads/ /usr/share/nginx/html/downloads/
EXPOSE 80
+925
View File
@@ -0,0 +1,925 @@
# JAILGAMES — Plan Técnico Completo
---
## 1. Arquitectura General
### Decisión: Eleventy (11ty) como generador de sitio estático
Se descartan tres alternativas y se elige una:
| Opción | Ventajas | Inconvenientes |
|---|---|---|
| HTML/CSS/JS puro | Sin dependencias | Hay que duplicar HTML por cada juego; cualquier cambio en el layout obliga a tocar todos los archivos |
| Astro / Vite | Moderno, componentes | Pesado para <10 juegos; requiere Node + build complejo |
| Microframework (Flask, Express) | Dinámico | Necesita servidor con runtime activo; innecesario para contenido estático en ferias |
| **Eleventy (11ty)** | **Plantillas Nunjucks, lee JSON/YAML nativamente, genera HTML puro, cero JS en el navegador** | **Requiere Node para el build (no en producción)** |
**Justificación concreta:**
- **Mantenimiento sencillo**: añadir un juego = crear un archivo YAML + copiar imágenes. Nada más.
- **Cero dependencias en producción**: el resultado es HTML/CSS/JS estático servido por Nginx.
- **Funciona sin internet**: perfecto para ferias presenciales. Se sirve desde un portátil con Docker.
- **Nginx en lugar de Apache**: más ligero, configuración mínima para servir estáticos. Si se prefiere Apache, el cambio es trivial (se indica en la sección Docker).
---
## 2. Estructura de Carpetas
```
jailgames/
├── src/ # ← Fuentes (nunca se sirven directamente)
│ ├── _data/
│ │ └── site.json # Datos globales (nombre del sitio, redes, etc.)
│ ├── _includes/
│ │ ├── base.njk # Layout base HTML (head, header, footer)
│ │ ├── game-card.njk # Componente: tarjeta de juego para la home
│ │ └── game-page.njk # Layout: ficha individual de juego
│ ├── games/ # Un archivo YAML por juego
│ │ ├── cosmic-escape.yaml
│ │ ├── pixel-dungeon.yaml
│ │ └── ...
│ ├── css/
│ │ └── style.css # Estilos globales
│ ├── js/
│ │ └── main.js # JS mínimo (lightbox, filtros, etc.)
│ ├── index.njk # Página principal (home)
│ └── game.njk # Plantilla que genera cada /game/slug/
├── static/ # ← Archivos copiados tal cual al build
│ ├── logo/
│ │ └── jailgames-logo.png # Logo general del sitio
│ ├── games/ # Carpeta por juego (nombre = slug)
│ │ ├── cosmic-escape/
│ │ │ ├── logo.png
│ │ │ ├── screenshot-1.png
│ │ │ ├── screenshot-2.png
│ │ │ └── screenshot-3.png
│ │ └── pixel-dungeon/
│ │ ├── logo.png
│ │ └── ...
│ └── favicon.ico
├── downloads/ # ← Binarios descargables
│ ├── cosmic-escape/
│ │ ├── cosmic-escape-v1.2-windows.zip
│ │ ├── cosmic-escape-v1.2-linux.tar.gz
│ │ └── cosmic-escape-v1.2-mac.dmg
│ └── pixel-dungeon/
│ └── ...
├── _site/ # ← Salida del build (se sirve con Nginx)
├── .eleventy.js # Configuración de Eleventy
├── package.json
├── Dockerfile
├── docker-compose.yml
├── nginx.conf # Configuración de Nginx
└── README.md
```
### Resumen de ubicaciones
| Contenido | Ruta |
|---|---|
| Datos de cada juego | `src/games/<slug>.yaml` |
| Imágenes de cada juego | `static/games/<slug>/` |
| Binarios descargables | `downloads/<slug>/` |
| Plantillas HTML | `src/_includes/` |
| Página principal | `src/index.njk` |
| Logo del sitio | `static/logo/jailgames-logo.png` |
| CSS | `src/css/style.css` |
| Salida final | `_site/` |
---
## 3. Formato de Datos por Juego
Cada juego se define en un archivo YAML independiente dentro de `src/games/`. El nombre del archivo es el **slug** (identificador URL).
### Ejemplo: `src/games/cosmic-escape.yaml`
```yaml
name: "Cosmic Escape"
slug: "cosmic-escape"
tagline: "Escapa de la estación antes de que colapse"
description: |
Un juego de plataformas 2D donde controlas a un astronauta atrapado en una
estación espacial que se desmorona. Recoge oxígeno, esquiva meteoritos y
encuentra la cápsula de escape antes de que se agote el tiempo.
Desarrollado en Godot Engine. Incluye 12 niveles y modo contrarreloj.
version: "1.2.0"
release_date: "2025-11-15"
tags:
- plataformas
- 2D
- singleplayer
logo: "/games/cosmic-escape/logo.png"
screenshots:
- "/games/cosmic-escape/screenshot-1.png"
- "/games/cosmic-escape/screenshot-2.png"
- "/games/cosmic-escape/screenshot-3.png"
downloads:
- platform: "Windows"
file: "/downloads/cosmic-escape/cosmic-escape-v1.2-windows.zip"
size: "85 MB"
- platform: "Linux"
file: "/downloads/cosmic-escape/cosmic-escape-v1.2-linux.tar.gz"
size: "78 MB"
- platform: "macOS"
file: "/downloads/cosmic-escape/cosmic-escape-v1.2-mac.dmg"
size: "90 MB"
# Campos opcionales
engine: "Godot 4.2"
players: "1 jugador"
genre: "Plataformas / Acción"
repo: "https://gitea.local/jailgames/cosmic-escape" # Solo referencia interna
```
### Campos obligatorios vs opcionales
| Campo | Obligatorio | Descripción |
|---|---|---|
| `name` | Sí | Nombre visible del juego |
| `slug` | Sí | Identificador para URLs y carpetas |
| `description` | Sí | Texto descriptivo (soporta párrafos) |
| `version` | Sí | Versión del juego |
| `release_date` | Sí | Fecha de publicación (YYYY-MM-DD) |
| `downloads` | Sí | Al menos una entrada con platform/file/size |
| `screenshots` | Sí | Al menos una captura |
| `logo` | No | Si no existe, se usa una imagen placeholder |
| `tagline` | No | Frase corta para la tarjeta de la home |
| `tags` | No | Etiquetas para filtrar |
| `engine` | No | Motor de desarrollo |
| `players` | No | Número de jugadores |
| `genre` | No | Género del juego |
| `repo` | No | Enlace al repositorio (solo referencia) |
---
## 4. Generación de Páginas
### 4.1 Configuración de Eleventy (`.eleventy.js`)
```javascript
const yaml = require("js-yaml");
const fs = require("fs");
const path = require("path");
module.exports = function (eleventyConfig) {
// Parsear archivos YAML
eleventyConfig.addDataExtension("yaml,yml", (contents) =>
yaml.load(contents)
);
// Copiar archivos estáticos al build
eleventyConfig.addPassthroughCopy("static");
eleventyConfig.addPassthroughCopy("downloads");
eleventyConfig.addPassthroughCopy("src/css");
eleventyConfig.addPassthroughCopy("src/js");
// Colección: todos los juegos ordenados por fecha
eleventyConfig.addCollection("games", function () {
const gamesDir = path.join(__dirname, "src", "games");
const files = fs.readdirSync(gamesDir).filter((f) => f.endsWith(".yaml"));
return files
.map((f) => {
const data = yaml.load(
fs.readFileSync(path.join(gamesDir, f), "utf-8")
);
return data;
})
.sort((a, b) => new Date(b.release_date) - new Date(a.release_date));
});
// Filtro para formatear fechas
eleventyConfig.addFilter("dateFormat", function (dateStr) {
const d = new Date(dateStr);
return d.toLocaleDateString("es-ES", {
year: "numeric",
month: "long",
day: "numeric",
});
});
// Filtro Markdown para descripciones
const markdownIt = require("markdown-it");
const md = markdownIt({ html: true });
eleventyConfig.addFilter("markdown", (content) => md.render(content || ""));
return {
dir: {
input: "src",
includes: "_includes",
data: "_data",
output: "_site",
},
templateFormats: ["njk", "md"],
htmlTemplateEngine: "njk",
};
};
```
### 4.2 Página Principal (`src/index.njk`)
La home itera sobre la colección `games` y renderiza una tarjeta por cada juego.
```html
---
layout: base.njk
title: "JAILGAMES — Nuestros Juegos"
---
<section class="hero">
<img src="/logo/jailgames-logo.png" alt="JAILGAMES" class="site-logo" />
<p class="hero-subtitle">Juegos caseros, hechos con cariño</p>
</section>
<section class="games-grid">
{% for game in collections.games %}
{% include "game-card.njk" %}
{% endfor %}
</section>
```
### 4.3 Tarjeta de Juego (`src/_includes/game-card.njk`)
```html
<a href="/game/{{ game.slug }}/" class="game-card">
<div class="game-card__image">
{% if game.logo %}
<img src="{{ game.logo }}" alt="{{ game.name }} logo" loading="lazy" />
{% else %}
<div class="game-card__placeholder">🎮</div>
{% endif %}
</div>
<div class="game-card__info">
<h2 class="game-card__title">{{ game.name }}</h2>
{% if game.tagline %}
<p class="game-card__tagline">{{ game.tagline }}</p>
{% endif %}
<div class="game-card__meta">
<span class="game-card__version">v{{ game.version }}</span>
{% if game.tags %}
{% for tag in game.tags %}
<span class="game-card__tag">{{ tag }}</span>
{% endfor %}
{% endif %}
</div>
</div>
</a>
```
### 4.4 Generación de Fichas Individuales (`src/game.njk`)
Eleventy genera una página por cada archivo YAML mediante **paginación**:
```html
---
pagination:
data: collections.games
size: 1
alias: game
permalink: "/game/{{ game.slug }}/"
layout: game-page.njk
eleventyComputed:
title: "{{ game.name }} — JAILGAMES"
---
```
### 4.5 Layout de Ficha (`src/_includes/game-page.njk`)
```html
{% extends "base.njk" %}
{% block content %}
<article class="game-detail">
<header class="game-detail__header">
{% if game.logo %}
<img src="{{ game.logo }}" alt="{{ game.name }}" class="game-detail__logo" />
{% endif %}
<div>
<h1>{{ game.name }}</h1>
{% if game.tagline %}<p class="game-detail__tagline">{{ game.tagline }}</p>{% endif %}
<div class="game-detail__meta">
<span>v{{ game.version }}</span>
<span>{{ game.release_date | dateFormat }}</span>
{% if game.genre %}<span>{{ game.genre }}</span>{% endif %}
{% if game.players %}<span>{{ game.players }}</span>{% endif %}
{% if game.engine %}<span>{{ game.engine }}</span>{% endif %}
</div>
</div>
</header>
<section class="game-detail__screenshots">
{% for img in game.screenshots %}
<img src="{{ img }}" alt="Captura de {{ game.name }}" loading="lazy"
class="game-detail__screenshot" />
{% endfor %}
</section>
<section class="game-detail__description">
{{ game.description | markdown | safe }}
</section>
<section class="game-detail__downloads">
<h2>Descargar</h2>
<div class="download-buttons">
{% for dl in game.downloads %}
<a href="{{ dl.file }}" class="download-btn" download>
<span class="download-btn__platform">{{ dl.platform }}</span>
<span class="download-btn__size">{{ dl.size }}</span>
</a>
{% endfor %}
</div>
</section>
{% if game.tags %}
<section class="game-detail__tags">
{% for tag in game.tags %}
<span class="tag">{{ tag }}</span>
{% endfor %}
</section>
{% endif %}
<a href="/" class="back-link">← Volver al catálogo</a>
</article>
{% endblock %}
```
### 4.6 Descargas Locales
Las descargas se enlazan directamente a archivos en `/downloads/<slug>/`. El atributo `download` en el `<a>` fuerza la descarga en lugar de abrir el archivo en el navegador. Nginx sirve estos archivos como estáticos. No hay enlaces a Gitea ni servicios externos.
---
## 5. Diseño y Estilo
### 5.1 Framework CSS: PicoCSS
Se recomienda **PicoCSS** (`@picocss/pico`) por:
- Estilo limpio sin necesidad de clases (aplica estilos directamente a elementos HTML semánticos).
- Muy ligero (~10 KB).
- Soporte nativo de tema oscuro/claro.
- Responsive por defecto.
- Sin dependencias de build (se puede usar como archivo CSS estático).
Si se prefiere más control visual, la alternativa es **Tailwind CSS** (requiere build step).
### 5.2 Esquema de Layout
```
┌──────────────────────────────────────────────┐
│ HEADER │
│ [Logo JAILGAMES] Juegos caseros... │
├──────────────────────────────────────────────┤
│ │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ Logo │ │ Logo │ │ Logo │ │
│ │ │ │ │ │ │ │
│ │ Name │ │ Name │ │ Name │ ← Grid │
│ │ Tag │ │ Tag │ │ Tag │ 3 cols │
│ └──────┘ └──────┘ └──────┘ │
│ │
│ ┌──────┐ ┌──────┐ │
│ │ ... │ │ ... │ │
│ └──────┘ └──────┘ │
│ │
├──────────────────────────────────────────────┤
│ FOOTER │
│ JAILGAMES · Hecho con ♥ │
└──────────────────────────────────────────────┘
```
**Ficha individual:**
```
┌──────────────────────────────────────────────┐
│ ← Volver HEADER │
├──────────────────────────────────────────────┤
│ │
│ [Logo] Nombre del Juego │
│ v1.2.0 · 15 nov 2025 · Godot │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Galería de capturas │ │
│ │ [img1] [img2] [img3] │ │
│ └──────────────────────────────────────┘ │
│ │
│ Descripción del juego en prosa... │
│ Párrafo 1... │
│ Párrafo 2... │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Windows │ │ Linux │ │ macOS │ │
│ │ 85 MB │ │ 78 MB │ │ 90 MB │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ #plataformas #2D #singleplayer │
│ │
├──────────────────────────────────────────────┤
│ FOOTER │
└──────────────────────────────────────────────┘
```
### 5.3 Diseño Responsive
- **Desktop (>1024px)**: Grid de 3 columnas para tarjetas.
- **Tablet (7681024px)**: Grid de 2 columnas.
- **Móvil (<768px)**: 1 columna, capturas en scroll horizontal.
### 5.4 Paleta de Colores Sugerida
```css
:root {
--color-bg: #0f0f0f;
--color-surface: #1a1a2e;
--color-primary: #e94560;
--color-secondary: #16213e;
--color-text: #eaeaea;
--color-text-muted: #a0a0a0;
--color-accent: #0f3460;
--color-success: #4ecca3;
--font-main: "Inter", "Segoe UI", sans-serif;
--font-heading: "Space Grotesk", "Inter", sans-serif;
}
```
Tema oscuro por defecto (estética gaming), con contraste suficiente para legibilidad.
---
## 6. Pipeline de Actualización
### Añadir un juego nuevo
```
Paso 1 Crear el archivo YAML
→ src/games/<slug>.yaml
(copiar de un juego existente y modificar campos)
Paso 2 Crear carpeta de imágenes
→ static/games/<slug>/
Copiar: logo.png, screenshot-1.png, screenshot-2.png, ...
Paso 3 Crear carpeta de descargas
→ downloads/<slug>/
Copiar binarios: <slug>-v<version>-<platform>.<ext>
Paso 4 Verificar que las rutas en el YAML coinciden con los archivos copiados.
Paso 5 Regenerar el sitio:
$ npm run build
(Esto ejecuta: npx @11ty/eleventy)
Paso 6 Verificar localmente:
$ npm run serve
(Esto ejecuta: npx @11ty/eleventy --serve)
Abrir http://localhost:8080 y comprobar.
Paso 7 Reconstruir el contenedor Docker:
$ docker compose up --build -d
```
### Modificar un juego existente
```
Paso 1 Editar src/games/<slug>.yaml
Paso 2 Si hay imágenes nuevas, copiarlas a static/games/<slug>/
Paso 3 Si hay binarios nuevos, copiarlos a downloads/<slug>/
Paso 4 Regenerar: npm run build
Paso 5 Reconstruir Docker: docker compose up --build -d
```
### Diagrama del flujo
```
[YAML nuevo] + [Imágenes] + [Binarios]
npm run build ← Eleventy lee YAML, genera HTML
_site/ ← HTML/CSS/JS estático listo
docker compose up ← Nginx sirve _site/ + downloads/
http://localhost ← Web lista para la feria
```
---
## 7. Plan de Docker
### 7.1 Dockerfile
```dockerfile
# ---------- ETAPA 1: Build ----------
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY src/ src/
COPY static/ static/
COPY .eleventy.js ./
RUN npx @11ty/eleventy
# ---------- ETAPA 2: Servir ----------
FROM nginx:alpine
# Configuración personalizada de Nginx
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copiar el sitio generado
COPY --from=builder /app/_site /usr/share/nginx/html
# Copiar descargas (no pasan por Eleventy)
COPY downloads/ /usr/share/nginx/html/downloads/
EXPOSE 80
```
### 7.2 docker-compose.yml
```yaml
version: "3.8"
services:
jailgames:
build: .
container_name: jailgames-web
ports:
- "80:80"
volumes:
# Volumen para descargas: permite actualizar binarios
# sin reconstruir la imagen completa
- ./downloads:/usr/share/nginx/html/downloads:ro
# Volumen para imágenes de juegos (opcional, misma idea)
- ./static/games:/usr/share/nginx/html/games:ro
restart: unless-stopped
```
### 7.3 nginx.conf
```nginx
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Compresión para HTML/CSS/JS
gzip on;
gzip_types text/html text/css application/javascript application/json image/svg+xml;
gzip_min_length 256;
# Cacheo agresivo para assets estáticos (feria local)
location ~* \.(png|jpg|jpeg|gif|svg|ico|webp)$ {
expires 7d;
add_header Cache-Control "public, immutable";
}
# Descargas: forzar descarga en lugar de abrir en navegador
location /downloads/ {
add_header Content-Disposition "attachment";
sendfile on;
tcp_nopush on;
}
# SPA fallback (por si se accede directamente a /game/slug/)
location / {
try_files $uri $uri/ $uri.html =404;
}
# Página 404 personalizada
error_page 404 /404.html;
}
```
### 7.4 Uso con Apache (alternativa)
Si se prefiere Apache, reemplazar la etapa 2 del Dockerfile:
```dockerfile
FROM httpd:alpine
COPY --from=builder /app/_site /usr/local/apache2/htdocs/
COPY downloads/ /usr/local/apache2/htdocs/downloads/
COPY apache.conf /usr/local/apache2/conf/httpd.conf
EXPOSE 80
```
### 7.5 Despliegue en Ferias sin Internet
```
1. En la máquina de desarrollo (con internet):
$ docker compose build
$ docker save jailgames-web:latest | gzip > jailgames-image.tar.gz
2. Copiar jailgames-image.tar.gz al portátil de la feria (USB).
3. En el portátil de la feria (sin internet):
$ gunzip -c jailgames-image.tar.gz | docker load
$ docker run -d -p 80:80 \
-v /ruta/local/downloads:/usr/share/nginx/html/downloads:ro \
--name jailgames jailgames-web:latest
4. Abrir http://localhost en cualquier navegador.
(O configurar un punto de acceso WiFi para que otros dispositivos
se conecten a la IP local del portátil)
```
---
## 8. Lista de Tareas para Claude Code
Copiar esta lista tal cual y dársela a Claude Code como instrucciones paso a paso.
---
### BLOQUE A — Inicialización del Proyecto
```
TAREA A1: Crear la estructura de carpetas del proyecto "jailgames"
Crear los siguientes directorios:
- src/_data/
- src/_includes/
- src/games/
- src/css/
- src/js/
- static/logo/
- static/games/
- downloads/
TAREA A2: Inicializar package.json
Ejecutar: npm init -y
Instalar dependencias:
npm install --save-dev @11ty/eleventy js-yaml markdown-it
Añadir scripts a package.json:
"build": "npx @11ty/eleventy",
"serve": "npx @11ty/eleventy --serve --port=8080",
"clean": "rm -rf _site"
```
### BLOQUE B — Configuración de Eleventy
```
TAREA B1: Crear .eleventy.js en la raíz del proyecto
Contenido: Configuración de Eleventy que:
- Parsea archivos YAML con js-yaml
- Copia "static" y "downloads" como passthrough
- Copia "src/css" y "src/js" como passthrough
- Crea una colección "games" leyendo todos los .yaml de src/games/
- Ordena los juegos por release_date (más recientes primero)
- Añade filtro "dateFormat" que formatea fechas en español
- Añade filtro "markdown" que convierte texto a HTML con markdown-it
- Input: src/, Output: _site/, Includes: _includes/, Data: _data/
TAREA B2: Crear src/_data/site.json
Contenido:
{
"name": "JAILGAMES",
"tagline": "Juegos caseros, hechos con cariño",
"url": "http://localhost",
"lang": "es"
}
```
### BLOQUE C — Plantillas HTML (Nunjucks)
```
TAREA C1: Crear src/_includes/base.njk
Layout base HTML5 que incluya:
- DOCTYPE html, lang="es"
- Meta charset, viewport
- Title dinámico: {{ title }} o fallback a site.name
- Enlace a /css/style.css
- Header con logo de JAILGAMES (enlazado a /)
- Bloque {% block content %}{% endblock %}
- Footer con "JAILGAMES · Hecho con ♥"
- Enlace a /js/main.js antes de cerrar body
TAREA C2: Crear src/_includes/game-card.njk
Componente tarjeta de juego:
- Enlace <a> a /game/{{ game.slug }}/
- Imagen del logo del juego (o placeholder si no hay logo)
- Nombre del juego <h2>
- Tagline si existe
- Versión y tags en un div de metadatos
TAREA C3: Crear src/_includes/game-page.njk
Layout para ficha individual. Extiende base.njk. Incluye:
- Header con logo del juego + nombre + tagline + metadatos
(versión, fecha formateada, género, players, engine)
- Sección de galería de capturas (iterar screenshots)
- Sección de descripción (usar filtro markdown)
- Sección de descargas: un botón por cada entrada en downloads
(con atributo download, mostrar plataforma y tamaño)
- Sección de tags
- Enlace "← Volver al catálogo" a /
TAREA C4: Crear src/index.njk
Página principal:
- Layout: base.njk
- Sección hero con logo y subtítulo
- Sección games-grid que itera collections.games
e incluye game-card.njk para cada juego
TAREA C5: Crear src/game.njk
Plantilla de paginación que genera /game/<slug>/ para cada juego:
- Front matter con pagination (data: collections.games, size: 1, alias: game)
- Permalink: /game/{{ game.slug }}/
- Layout: game-page.njk
- eleventyComputed title
```
### BLOQUE D — Estilos CSS
```
TAREA D1: Crear src/css/style.css
Archivo CSS completo que incluya:
- Variables CSS (colores tema oscuro gaming, fuentes)
- Reset mínimo (box-sizing, margin 0)
- Body: fondo oscuro, color claro, fuente sans-serif
- .site-header: flex, centrado, con logo a la izquierda
- .site-logo: max-height 60px
- .hero: centrado, padding generoso
- .hero-subtitle: color muted
- .games-grid: CSS Grid, 3 columnas en desktop, 2 en tablet, 1 en móvil
- .game-card: bloque con fondo surface, borde redondeado, hover con sombra,
transición suave. Sin text-decoration.
- .game-card__image img: object-fit contain, altura fija
- .game-card__title: color blanco
- .game-card__tagline: color muted, font-size pequeño
- .game-card__tag: badge pequeño con color accent
- .game-detail: max-width 900px, centrado, padding
- .game-detail__header: flex, logo a la izquierda, info a la derecha
- .game-detail__screenshots: grid o flex con scroll horizontal en móvil
- .game-detail__screenshot: borde redondeado, max-width 100%
- .download-buttons: flex, wrap
- .download-btn: botón grande con color primary, hover,
icono de plataforma si es posible
- .tag: badge igual que game-card__tag
- .back-link: estilo enlace simple
- .site-footer: centrado, padding, color muted
- Media queries para responsive (768px, 1024px)
```
### BLOQUE E — JavaScript Mínimo
```
TAREA E1: Crear src/js/main.js
Funcionalidad mínima:
- Lightbox simple para capturas: al hacer clic en una screenshot,
mostrarla en overlay a pantalla completa. Clic en overlay = cerrar.
- (Opcional) Filtrado de juegos por tags en la home.
- No usar frameworks, vanilla JS puro.
```
### BLOQUE F — Datos de Ejemplo
```
TAREA F1: Crear 2 archivos YAML de ejemplo en src/games/
Archivo 1: src/games/cosmic-escape.yaml
- Rellenar con datos ficticios pero realistas
- Incluir al menos 3 screenshots, 2 plataformas de descarga
- Incluir todos los campos opcionales como ejemplo
Archivo 2: src/games/pixel-dungeon.yaml
- Rellenar con datos ficticios diferentes
- Incluir solo campos obligatorios (ejemplo minimalista)
TAREA F2: Crear imágenes placeholder
- static/logo/jailgames-logo.png → placeholder SVG o imagen genérica
- static/games/cosmic-escape/logo.png → placeholder
- static/games/cosmic-escape/screenshot-1.png → placeholder 800x450
- static/games/cosmic-escape/screenshot-2.png → placeholder 800x450
- static/games/pixel-dungeon/logo.png → placeholder
(Pueden ser SVGs generados con texto identificativo)
TAREA F3: Crear archivos de descarga de ejemplo
- downloads/cosmic-escape/README.txt
(archivo de texto que diga "Aquí irá el binario de Cosmic Escape")
- downloads/pixel-dungeon/README.txt
(archivo de texto que diga "Aquí irá el binario de Pixel Dungeon")
```
### BLOQUE G — Página 404
```
TAREA G1: Crear src/404.njk
Página de error 404:
- Layout: base.njk
- Permalink: /404.html
- Mensaje amigable: "Página no encontrada"
- Enlace de vuelta a la home
```
### BLOQUE H — Docker
```
TAREA H1: Crear Dockerfile en la raíz
Multi-stage build:
- Etapa 1 (builder): node:20-alpine, npm ci, npx @11ty/eleventy
- Etapa 2 (serve): nginx:alpine, copiar _site y downloads, copiar nginx.conf
TAREA H2: Crear nginx.conf en la raíz
Configuración Nginx:
- Puerto 80, root /usr/share/nginx/html
- Gzip activado para text/html, text/css, application/javascript
- Cache de imágenes 7 días
- Descargas con Content-Disposition: attachment
- try_files para rutas limpias
- error_page 404 → /404.html
TAREA H3: Crear docker-compose.yml en la raíz
Servicio jailgames:
- Build desde el directorio actual
- Puerto 80:80
- Volumen downloads como read-only
- restart: unless-stopped
```
### BLOQUE I — Documentación
```
TAREA I1: Crear README.md en la raíz
Incluir:
- Nombre del proyecto y descripción breve
- Requisitos previos (Node.js 18+, Docker)
- Cómo instalar dependencias (npm ci)
- Cómo ejecutar en desarrollo (npm run serve)
- Cómo hacer build (npm run build)
- Cómo añadir un juego nuevo (los 7 pasos del pipeline)
- Cómo desplegar con Docker (docker compose up --build)
- Cómo desplegar sin internet (docker save/load)
- Estructura de carpetas resumida
```
### BLOQUE J — Verificación Final
```
TAREA J1: Ejecutar npm run build y verificar que _site/ se genera correctamente.
TAREA J2: Ejecutar npm run serve y verificar en http://localhost:8080:
- La home muestra las tarjetas de los 2 juegos de ejemplo
- Hacer clic en una tarjeta lleva a la ficha individual
- La ficha muestra logo, capturas, descripción y botones de descarga
- Los botones de descarga funcionan (descargan el archivo)
- El enlace "Volver" funciona
- La página 404 funciona al visitar una ruta inexistente
- El diseño es responsive (probar en móvil con DevTools)
TAREA J3: Ejecutar docker compose up --build y verificar en http://localhost:80
que todo funciona igual que en J2.
```
---
## Resumen Ejecutivo
| Aspecto | Decisión |
|---|---|
| Generador | Eleventy (11ty) |
| Formato de datos | YAML (1 archivo por juego) |
| Plantillas | Nunjucks |
| CSS | CSS custom con tema oscuro (opcionalmente PicoCSS) |
| Servidor | Nginx en Docker |
| Build | Multi-stage Docker (Node → Nginx) |
| Tiempo para añadir un juego | ~5 minutos (crear YAML + copiar archivos + rebuild) |
| Funciona sin internet | Sí (docker save/load) |
+151
View File
@@ -0,0 +1,151 @@
# JAILGAMES
Sitio web estático para publicar y descargar juegos indie del estudio JAILGAMES.
Generado con **Eleventy (11ty)** y servido con **Nginx en Docker**.
---
## Requisitos previos
- **Node.js 18+** (para el build local)
- **Docker** + **Docker Compose** (para despliegue)
---
## Instalación
```bash
npm ci
```
---
## Desarrollo local
```bash
npm run serve
```
Abre [http://localhost:8080](http://localhost:8080) en el navegador.
Eleventy reconstruye automáticamente al guardar cambios.
---
## Build estático
```bash
npm run build
```
El resultado queda en `_site/`. Para limpiar:
```bash
npm run clean
```
---
## Añadir un juego nuevo
1. **Crear el archivo YAML** del juego:
```
src/games/<slug>.yaml
```
Copiar de un juego existente y modificar los campos. El `slug` es el identificador URL (solo letras minúsculas, números y guiones).
2. **Crear la carpeta de imágenes:**
```
static/games/<slug>/
```
Copiar: `logo.png` (o `.svg`), `screenshot-1.png`, `screenshot-2.png`, ...
3. **Crear la carpeta de descargas:**
```
downloads/<slug>/
```
Copiar los binarios: `<slug>-v<version>-<plataforma>.<ext>`
4. **Verificar** que las rutas en el YAML coinciden exactamente con los archivos copiados.
5. **Regenerar el sitio:**
```bash
npm run build
```
6. **Verificar localmente:**
```bash
npm run serve
```
Abrir [http://localhost:8080](http://localhost:8080) y comprobar el juego nuevo.
7. **Reconstruir el contenedor Docker:**
```bash
docker compose up --build -d
```
---
## Despliegue con Docker
```bash
docker compose up --build -d
```
Acceder en [http://localhost](http://localhost).
Para detener:
```bash
docker compose down
```
---
## Despliegue sin internet (ferias)
**En la máquina con internet:**
```bash
docker compose build
docker save jailgames-web:latest | gzip > jailgames-image.tar.gz
```
**Copiar** `jailgames-image.tar.gz` al portátil de la feria (USB).
**En el portátil de la feria (sin internet):**
```bash
gunzip -c jailgames-image.tar.gz | docker load
docker run -d -p 80:80 \
-v /ruta/local/downloads:/usr/share/nginx/html/downloads:ro \
--name jailgames jailgames-web:latest
```
Abrir [http://localhost](http://localhost) en cualquier navegador.
---
## Estructura de carpetas
```
jailgames/
├── src/
│ ├── _data/site.json # Datos globales del sitio
│ ├── _includes/
│ │ ├── base.njk # Layout base HTML
│ │ ├── game-card.njk # Tarjeta de juego (home)
│ │ └── game-page.njk # Ficha individual de juego
│ ├── games/ # Un archivo YAML por juego
│ ├── css/style.css # Estilos globales
│ ├── js/main.js # JS mínimo (lightbox)
│ ├── index.njk # Página principal
│ ├── game.njk # Generador de fichas individuales
│ └── 404.njk # Página de error 404
├── static/
│ ├── logo/ # Logo del sitio
│ └── games/<slug>/ # Imágenes de cada juego
├── downloads/<slug>/ # Binarios descargables
├── _site/ # Build generado (no versionar)
├── .eleventy.js # Configuración de Eleventy
├── package.json
├── Dockerfile
├── docker-compose.yml
└── nginx.conf
```
+12
View File
@@ -0,0 +1,12 @@
services:
jailgames:
build: .
container_name: jailgames-web
ports:
- "80:80"
volumes:
# Permite actualizar binarios sin reconstruir la imagen completa
- ./downloads:/usr/share/nginx/html/downloads:ro
# Permite actualizar imágenes de juegos sin reconstruir
- ./static/games:/usr/share/nginx/html/games:ro
restart: unless-stopped
+15
View File
@@ -0,0 +1,15 @@
Aquí irá el binario de Cosmic Escape.
Nombre del juego: Cosmic Escape
Versión: 1.2.0
Motor: Godot 4.2
Plataformas disponibles:
- Windows: cosmic-escape-v1.2-windows.zip (~85 MB)
- Linux: cosmic-escape-v1.2-linux.tar.gz (~78 MB)
- macOS: cosmic-escape-v1.2-mac.dmg (~90 MB)
Instrucciones de instalación:
Windows: Descomprimir el ZIP y ejecutar CosmiEscape.exe
Linux: Descomprimir el tar.gz y ejecutar ./cosmic-escape
macOS: Abrir el DMG y arrastrar la app a Aplicaciones
+10
View File
@@ -0,0 +1,10 @@
Aquí irá el binario de Pixel Dungeon.
Nombre del juego: Pixel Dungeon
Versión: 0.9.1
Plataformas disponibles:
- Windows: pixel-dungeon-v0.9.1-windows.zip (~22 MB)
Instrucciones de instalación:
Windows: Descomprimir el ZIP y ejecutar PixelDungeon.exe
+33
View File
@@ -0,0 +1,33 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Compresión para HTML/CSS/JS
gzip on;
gzip_types text/html text/css application/javascript application/json image/svg+xml;
gzip_min_length 256;
# Cacheo para assets estáticos (feria local)
location ~* \.(png|jpg|jpeg|gif|svg|ico|webp)$ {
expires 7d;
add_header Cache-Control "public, immutable";
}
# Descargas: forzar descarga en lugar de abrir en navegador
location /downloads/ {
add_header Content-Disposition "attachment";
sendfile on;
tcp_nopush on;
}
# Rutas limpias
location / {
try_files $uri $uri/ $uri.html =404;
}
# Página 404 personalizada
error_page 404 /404.html;
}
+1525
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
{
"name": "web_jailgames",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "npx @11ty/eleventy",
"serve": "npx @11ty/eleventy --serve --port=8080",
"clean": "rm -rf _site"
},
"repository": {
"type": "git",
"url": "https://gitea.sustancia.synology.me/jaildesigner-utilitats/web_jailgames.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@11ty/eleventy": "^3.1.5",
"js-yaml": "^4.1.1",
"markdown-it": "^14.1.1"
}
}
+12
View File
@@ -0,0 +1,12 @@
---
layout: base.njk
title: "404 — Página no encontrada — JAILGAMES"
permalink: /404.html
---
<div class="not-found">
<h1>404</h1>
<h2>Página no encontrada</h2>
<p>La página que buscas no existe o ha sido movida.</p>
<a href="/" class="download-btn" style="display:inline-flex;">&#8592; Volver al catálogo</a>
</div>
+6
View File
@@ -0,0 +1,6 @@
{
"name": "JAILGAMES",
"tagline": "Juegos caseros, hechos con cariño",
"url": "http://localhost",
"lang": "es"
}
+29
View File
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="{{ site.lang | default('es') }}">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ title if title else site.name }}</title>
<link rel="stylesheet" href="/css/style.css" />
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
</head>
<body>
<header class="site-header">
<a href="/" class="site-header__logo-link">
<img src="/logo/jailgames-logo.svg" alt="{{ site.name }}" class="site-logo" />
<span class="site-header__name">{{ site.name }}</span>
</a>
</header>
<main class="site-main">
{{ content | safe }}
</main>
<footer class="site-footer">
<p>{{ site.name }} &middot; Hecho con &#x2665;</p>
</footer>
<script src="/js/main.js"></script>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
<a href="/game/{{ game.slug }}/" class="game-card">
<div class="game-card__image">
{% if game.logo %}
<img src="{{ game.logo }}" alt="{{ game.name }} logo" loading="lazy" />
{% else %}
<div class="game-card__placeholder">🎮</div>
{% endif %}
</div>
<div class="game-card__info">
<h2 class="game-card__title">{{ game.name }}</h2>
{% if game.tagline %}
<p class="game-card__tagline">{{ game.tagline }}</p>
{% endif %}
<div class="game-card__meta">
<span class="game-card__version">v{{ game.version }}</span>
{% if game.tags %}
{% for tag in game.tags %}
<span class="game-card__tag">{{ tag }}</span>
{% endfor %}
{% endif %}
</div>
</div>
</a>
+60
View File
@@ -0,0 +1,60 @@
---
layout: base.njk
---
<article class="game-detail">
<header class="game-detail__header">
{% if game.logo %}
<img src="{{ game.logo }}" alt="{{ game.name }}" class="game-detail__logo" />
{% endif %}
<div class="game-detail__header-info">
<h1>{{ game.name }}</h1>
{% if game.tagline %}
<p class="game-detail__tagline">{{ game.tagline }}</p>
{% endif %}
<div class="game-detail__meta">
<span>v{{ game.version }}</span>
<span>{{ game.release_date | dateFormat }}</span>
{% if game.genre %}<span>{{ game.genre }}</span>{% endif %}
{% if game.players %}<span>{{ game.players }}</span>{% endif %}
{% if game.engine %}<span>{{ game.engine }}</span>{% endif %}
</div>
</div>
</header>
{% if game.screenshots and game.screenshots.length %}
<section class="game-detail__screenshots">
{% for img in game.screenshots %}
<img src="{{ img }}" alt="Captura {{ loop.index }} de {{ game.name }}"
loading="lazy" class="game-detail__screenshot" />
{% endfor %}
</section>
{% endif %}
<section class="game-detail__description">
{{ game.description | markdown | safe }}
</section>
<section class="game-detail__downloads">
<h2>Descargar</h2>
<div class="download-buttons">
{% for dl in game.downloads %}
<a href="{{ dl.file }}" class="download-btn" download>
<span class="download-btn__platform">{{ dl.platform }}</span>
<span class="download-btn__size">{{ dl.size }}</span>
</a>
{% endfor %}
</div>
</section>
{% if game.tags %}
<section class="game-detail__tags">
{% for tag in game.tags %}
<span class="tag">{{ tag }}</span>
{% endfor %}
</section>
{% endif %}
<a href="/" class="back-link">&#8592; Volver al catálogo</a>
</article>
+531
View File
@@ -0,0 +1,531 @@
/* =============================================
VARIABLES — Tema oscuro gaming
============================================= */
:root {
--color-bg: #0f0f0f;
--color-surface: #1a1a2e;
--color-surface-hover: #22223a;
--color-primary: #e94560;
--color-primary-hover: #c73650;
--color-secondary: #16213e;
--color-text: #eaeaea;
--color-text-muted: #a0a0a0;
--color-accent: #0f3460;
--color-success: #4ecca3;
--color-border: #2a2a4a;
--font-main: "Segoe UI", "Inter", system-ui, sans-serif;
--font-heading: "Segoe UI", "Inter", system-ui, sans-serif;
--radius: 8px;
--radius-lg: 12px;
--shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
--shadow-hover: 0 8px 32px rgba(233, 69, 96, 0.3);
--transition: 0.2s ease;
}
/* =============================================
RESET MÍNIMO
============================================= */
*, *::before, *::after {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
}
img {
max-width: 100%;
display: block;
}
a {
color: var(--color-primary);
}
/* =============================================
BASE
============================================= */
body {
background-color: var(--color-bg);
color: var(--color-text);
font-family: var(--font-main);
font-size: 16px;
line-height: 1.6;
min-height: 100vh;
display: flex;
flex-direction: column;
}
h1, h2, h3 {
font-family: var(--font-heading);
margin: 0 0 0.5em;
line-height: 1.2;
}
/* =============================================
HEADER
============================================= */
.site-header {
background-color: var(--color-secondary);
border-bottom: 1px solid var(--color-border);
padding: 0.75rem 2rem;
display: flex;
align-items: center;
}
.site-header__logo-link {
display: flex;
align-items: center;
gap: 0.75rem;
text-decoration: none;
color: var(--color-text);
}
.site-logo {
max-height: 48px;
width: auto;
object-fit: contain;
}
.site-header__name {
font-family: var(--font-heading);
font-size: 1.4rem;
font-weight: 700;
letter-spacing: 0.05em;
color: var(--color-primary);
}
/* =============================================
MAIN
============================================= */
.site-main {
flex: 1;
padding: 2rem 1.5rem;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
/* =============================================
HERO
============================================= */
.hero {
text-align: center;
padding: 3rem 1rem 2rem;
}
.hero-logo {
max-height: 120px;
margin: 0 auto 1.5rem;
}
.hero-subtitle {
font-size: 1.2rem;
color: var(--color-text-muted);
margin: 0;
}
/* =============================================
GAMES GRID
============================================= */
.games-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
padding: 1rem 0;
}
/* =============================================
GAME CARD
============================================= */
.game-card {
display: flex;
flex-direction: column;
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
overflow: hidden;
text-decoration: none;
color: var(--color-text);
transition: transform var(--transition), box-shadow var(--transition), border-color var(--transition);
}
.game-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-hover);
border-color: var(--color-primary);
}
.game-card__image {
width: 100%;
height: 200px;
background-color: var(--color-accent);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.game-card__image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.game-card__placeholder {
font-size: 4rem;
line-height: 1;
}
.game-card__info {
padding: 1rem 1.25rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
flex: 1;
}
.game-card__title {
font-size: 1.1rem;
font-weight: 700;
color: var(--color-text);
margin: 0;
}
.game-card__tagline {
font-size: 0.875rem;
color: var(--color-text-muted);
margin: 0;
line-height: 1.4;
}
.game-card__meta {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin-top: auto;
padding-top: 0.5rem;
}
.game-card__version {
font-size: 0.75rem;
color: var(--color-success);
font-weight: 600;
}
.game-card__tag {
font-size: 0.7rem;
background-color: var(--color-accent);
color: var(--color-text);
border-radius: 4px;
padding: 0.15em 0.5em;
text-transform: lowercase;
}
/* =============================================
GAME DETAIL
============================================= */
.game-detail {
max-width: 900px;
margin: 0 auto;
padding: 1rem 0;
}
.game-detail__header {
display: flex;
align-items: flex-start;
gap: 1.5rem;
margin-bottom: 2rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--color-border);
}
.game-detail__logo {
width: 100px;
height: 100px;
object-fit: contain;
border-radius: var(--radius);
background-color: var(--color-accent);
flex-shrink: 0;
}
.game-detail__header-info {
flex: 1;
}
.game-detail__header-info h1 {
font-size: 2rem;
margin-bottom: 0.25rem;
}
.game-detail__tagline {
font-size: 1rem;
color: var(--color-text-muted);
margin: 0 0 0.75rem;
}
.game-detail__meta {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.game-detail__meta span {
font-size: 0.8rem;
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 4px;
padding: 0.2em 0.6em;
color: var(--color-text-muted);
}
/* =============================================
SCREENSHOTS
============================================= */
.game-detail__screenshots {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
overflow-x: auto;
padding-bottom: 0.5rem;
scrollbar-width: thin;
scrollbar-color: var(--color-border) transparent;
}
.game-detail__screenshot {
width: 320px;
min-width: 280px;
height: 180px;
object-fit: cover;
border-radius: var(--radius);
border: 1px solid var(--color-border);
cursor: pointer;
transition: transform var(--transition), box-shadow var(--transition);
flex-shrink: 0;
}
.game-detail__screenshot:hover {
transform: scale(1.02);
box-shadow: var(--shadow);
}
/* =============================================
DESCRIPCIÓN
============================================= */
.game-detail__description {
margin-bottom: 2rem;
line-height: 1.8;
}
.game-detail__description p {
margin: 0 0 1em;
color: var(--color-text);
}
/* =============================================
DESCARGAS
============================================= */
.game-detail__downloads {
margin-bottom: 2rem;
}
.game-detail__downloads h2 {
font-size: 1.3rem;
margin-bottom: 1rem;
color: var(--color-text);
}
.download-buttons {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.download-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0.75rem 1.5rem;
background-color: var(--color-primary);
color: #fff;
text-decoration: none;
border-radius: var(--radius);
font-weight: 600;
transition: background-color var(--transition), transform var(--transition);
min-width: 120px;
gap: 0.2rem;
}
.download-btn:hover {
background-color: var(--color-primary-hover);
transform: translateY(-2px);
}
.download-btn__platform {
font-size: 0.95rem;
}
.download-btn__size {
font-size: 0.75rem;
opacity: 0.85;
font-weight: 400;
}
/* =============================================
TAGS
============================================= */
.game-detail__tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 2rem;
}
.tag {
font-size: 0.8rem;
background-color: var(--color-accent);
color: var(--color-text);
border-radius: 4px;
padding: 0.25em 0.7em;
text-transform: lowercase;
}
/* =============================================
BACK LINK
============================================= */
.back-link {
display: inline-block;
margin-top: 1rem;
color: var(--color-text-muted);
text-decoration: none;
font-size: 0.9rem;
transition: color var(--transition);
}
.back-link:hover {
color: var(--color-primary);
}
/* =============================================
FOOTER
============================================= */
.site-footer {
background-color: var(--color-secondary);
border-top: 1px solid var(--color-border);
text-align: center;
padding: 1.5rem;
color: var(--color-text-muted);
font-size: 0.875rem;
}
/* =============================================
404
============================================= */
.not-found {
text-align: center;
padding: 4rem 1rem;
}
.not-found h1 {
font-size: 5rem;
color: var(--color-primary);
margin-bottom: 0.25rem;
}
.not-found p {
color: var(--color-text-muted);
margin-bottom: 2rem;
}
/* =============================================
LIGHTBOX
============================================= */
.lightbox-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.92);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
cursor: zoom-out;
padding: 1rem;
}
.lightbox-overlay img {
max-width: 95vw;
max-height: 90vh;
object-fit: contain;
border-radius: var(--radius);
box-shadow: var(--shadow);
cursor: default;
}
.lightbox-close {
position: absolute;
top: 1.5rem;
right: 1.5rem;
background: none;
border: none;
color: #fff;
font-size: 2rem;
cursor: pointer;
line-height: 1;
padding: 0.25rem 0.5rem;
opacity: 0.8;
transition: opacity var(--transition);
}
.lightbox-close:hover {
opacity: 1;
}
/* =============================================
RESPONSIVE — Tablet (≤ 1024px)
============================================= */
@media (max-width: 1024px) {
.games-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* =============================================
RESPONSIVE — Móvil (≤ 768px)
============================================= */
@media (max-width: 768px) {
.site-main {
padding: 1rem;
}
.games-grid {
grid-template-columns: 1fr;
}
.game-detail__header {
flex-direction: column;
align-items: center;
text-align: center;
}
.game-detail__meta {
justify-content: center;
}
.game-detail__header-info h1 {
font-size: 1.5rem;
}
.download-buttons {
flex-direction: column;
}
.download-btn {
width: 100%;
}
.hero-logo {
max-height: 80px;
}
}
+10
View File
@@ -0,0 +1,10 @@
---
pagination:
data: collections.games
size: 1
alias: game
permalink: "/game/{{ game.slug }}/"
layout: game-page.njk
eleventyComputed:
title: "{{ game.name }} — JAILGAMES"
---
+40
View File
@@ -0,0 +1,40 @@
name: "Cosmic Escape"
slug: "cosmic-escape"
tagline: "Escapa de la estación antes de que colapse"
description: |
Un juego de plataformas 2D donde controlas a un astronauta atrapado en una
estación espacial que se desmorona. Recoge oxígeno, esquiva meteoritos y
encuentra la cápsula de escape antes de que se agote el tiempo.
Desarrollado en Godot Engine. Incluye 12 niveles y modo contrarreloj.
Ideal para partidas cortas e intensas.
version: "1.2.0"
release_date: "2025-11-15"
tags:
- plataformas
- 2D
- singleplayer
logo: "/games/cosmic-escape/logo.svg"
screenshots:
- "/games/cosmic-escape/screenshot-1.svg"
- "/games/cosmic-escape/screenshot-2.svg"
- "/games/cosmic-escape/screenshot-3.svg"
downloads:
- platform: "Windows"
file: "/downloads/cosmic-escape/README.txt"
size: "85 MB"
- platform: "Linux"
file: "/downloads/cosmic-escape/README.txt"
size: "78 MB"
- platform: "macOS"
file: "/downloads/cosmic-escape/README.txt"
size: "90 MB"
engine: "Godot 4.2"
players: "1 jugador"
genre: "Plataformas / Acción"
repo: "https://gitea.local/jailgames/cosmic-escape"
+23
View File
@@ -0,0 +1,23 @@
name: "Pixel Dungeon"
slug: "pixel-dungeon"
description: |
Un roguelike de mazmorras con gráficos pixel art de 16x16. Explora
generaciones procedurales de niveles, recoge objetos y derrota al jefe
final. Cada partida es única.
version: "0.9.1"
release_date: "2025-08-03"
tags:
- roguelike
- pixel-art
- singleplayer
logo: "/games/pixel-dungeon/logo.svg"
screenshots:
- "/games/pixel-dungeon/screenshot-1.svg"
downloads:
- platform: "Windows"
file: "/downloads/pixel-dungeon/README.txt"
size: "22 MB"
+15
View File
@@ -0,0 +1,15 @@
---
layout: base.njk
title: "JAILGAMES — Nuestros Juegos"
---
<section class="hero">
<img src="/logo/jailgames-logo.svg" alt="JAILGAMES" class="site-logo hero-logo" />
<p class="hero-subtitle">{{ site.tagline }}</p>
</section>
<section class="games-grid">
{% for game in collections.games %}
{% include "game-card.njk" %}
{% endfor %}
</section>
+59
View File
@@ -0,0 +1,59 @@
(function () {
"use strict";
// ── Lightbox para capturas de pantalla ──────────────────────────────────
function initLightbox() {
const screenshots = document.querySelectorAll(".game-detail__screenshot");
if (!screenshots.length) return;
const overlay = document.createElement("div");
overlay.className = "lightbox-overlay";
overlay.setAttribute("role", "dialog");
overlay.setAttribute("aria-modal", "true");
overlay.setAttribute("aria-label", "Vista ampliada de captura");
const img = document.createElement("img");
const closeBtn = document.createElement("button");
closeBtn.className = "lightbox-close";
closeBtn.textContent = "\u00D7";
closeBtn.setAttribute("aria-label", "Cerrar");
overlay.appendChild(img);
overlay.appendChild(closeBtn);
function open(src, alt) {
img.src = src;
img.alt = alt;
document.body.appendChild(overlay);
document.body.style.overflow = "hidden";
closeBtn.focus();
}
function close() {
if (overlay.parentNode) {
document.body.removeChild(overlay);
document.body.style.overflow = "";
}
}
screenshots.forEach(function (el) {
el.addEventListener("click", function () {
open(el.src, el.alt);
});
});
overlay.addEventListener("click", function (e) {
if (e.target === overlay) close();
});
closeBtn.addEventListener("click", close);
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") close();
});
}
document.addEventListener("DOMContentLoaded", function () {
initLightbox();
});
})();
+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#0f3460" rx="12"/>
<text x="100" y="90" text-anchor="middle" font-family="monospace" font-size="56" fill="#e94560">🚀</text>
<text x="100" y="130" text-anchor="middle" font-family="monospace" font-size="13" fill="#eaeaea">COSMIC ESCAPE</text>
<text x="100" y="155" text-anchor="middle" font-family="monospace" font-size="11" fill="#4ecca3">v1.2.0</text>
</svg>

After

Width:  |  Height:  |  Size: 496 B

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="450" viewBox="0 0 800 450">
<rect width="800" height="450" fill="#0a0a1a"/>
<rect x="0" y="380" width="800" height="70" fill="#1a1a2e"/>
<circle cx="120" cy="80" r="40" fill="#f4d03f" opacity="0.8"/>
<circle cx="300" cy="50" r="8" fill="#eaeaea" opacity="0.4"/>
<circle cx="500" cy="120" r="5" fill="#eaeaea" opacity="0.5"/>
<circle cx="700" cy="30" r="10" fill="#eaeaea" opacity="0.3"/>
<rect x="350" y="250" width="40" height="60" fill="#4ecca3"/>
<rect x="360" y="220" width="20" height="30" fill="#a0a0a0"/>
<text x="400" y="430" text-anchor="middle" font-family="monospace" font-size="18" fill="#e94560">COSMIC ESCAPE — Nivel 1</text>
<text x="400" y="230" text-anchor="middle" font-family="monospace" font-size="12" fill="#a0a0a0">[Captura 1 — Placeholder]</text>
</svg>

After

Width:  |  Height:  |  Size: 861 B

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="450" viewBox="0 0 800 450">
<rect width="800" height="450" fill="#16213e"/>
<rect x="50" y="200" width="700" height="20" fill="#0f3460"/>
<rect x="50" y="300" width="300" height="20" fill="#0f3460"/>
<rect x="500" y="320" width="250" height="20" fill="#0f3460"/>
<circle cx="200" cy="190" r="15" fill="#e94560" opacity="0.9"/>
<circle cx="400" cy="290" r="15" fill="#e94560" opacity="0.7"/>
<rect x="370" y="160" width="30" height="40" fill="#4ecca3"/>
<text x="400" y="430" text-anchor="middle" font-family="monospace" font-size="18" fill="#e94560">COSMIC ESCAPE — Nivel 4</text>
<text x="400" y="60" text-anchor="middle" font-family="monospace" font-size="12" fill="#a0a0a0">[Captura 2 — Placeholder]</text>
</svg>

After

Width:  |  Height:  |  Size: 799 B

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="450" viewBox="0 0 800 450">
<rect width="800" height="450" fill="#1a0a0a"/>
<rect x="0" y="350" width="800" height="100" fill="#2a1a1a"/>
<ellipse cx="400" cy="200" rx="200" ry="120" fill="#e94560" opacity="0.15"/>
<circle cx="400" cy="200" r="60" fill="#c73650" opacity="0.6"/>
<text x="400" y="208" text-anchor="middle" font-family="monospace" font-size="36" fill="#eaeaea">👾</text>
<rect x="360" y="330" width="30" height="20" fill="#4ecca3"/>
<text x="400" y="430" text-anchor="middle" font-family="monospace" font-size="18" fill="#e94560">COSMIC ESCAPE — Jefe Final</text>
<text x="400" y="50" text-anchor="middle" font-family="monospace" font-size="12" fill="#a0a0a0">[Captura 3 — Placeholder]</text>
</svg>

After

Width:  |  Height:  |  Size: 797 B

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#16213e" rx="12"/>
<rect x="60" y="60" width="20" height="20" fill="#4ecca3"/>
<rect x="80" y="60" width="20" height="20" fill="#4ecca3"/>
<rect x="100" y="60" width="20" height="20" fill="#4ecca3"/>
<rect x="60" y="80" width="20" height="20" fill="#4ecca3"/>
<rect x="100" y="80" width="20" height="20" fill="#4ecca3"/>
<rect x="60" y="100" width="20" height="20" fill="#4ecca3"/>
<rect x="80" y="100" width="20" height="20" fill="#4ecca3"/>
<rect x="100" y="100" width="20" height="20" fill="#4ecca3"/>
<text x="100" y="150" text-anchor="middle" font-family="monospace" font-size="11" fill="#eaeaea">PIXEL DUNGEON</text>
<text x="100" y="170" text-anchor="middle" font-family="monospace" font-size="10" fill="#a0a0a0">v0.9.1</text>
</svg>

After

Width:  |  Height:  |  Size: 888 B

@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="450" viewBox="0 0 800 450">
<rect width="800" height="450" fill="#0a0a12"/>
<!-- Cuadrícula de tiles -->
<rect x="100" y="100" width="600" height="260" fill="#16213e"/>
<!-- Paredes -->
<rect x="100" y="100" width="600" height="20" fill="#0f3460"/>
<rect x="100" y="340" width="600" height="20" fill="#0f3460"/>
<rect x="100" y="100" width="20" height="260" fill="#0f3460"/>
<rect x="680" y="100" width="20" height="260" fill="#0f3460"/>
<!-- Pasillos -->
<rect x="300" y="100" width="20" height="80" fill="#0a0a12"/>
<rect x="480" y="280" width="20" height="80" fill="#0a0a12"/>
<!-- Jugador -->
<rect x="190" y="200" width="16" height="16" fill="#4ecca3"/>
<rect x="194" y="196" width="8" height="8" fill="#eaeaea"/>
<!-- Enemigo -->
<rect x="500" y="230" width="16" height="16" fill="#e94560"/>
<rect x="504" y="226" width="8" height="8" fill="#a02040"/>
<!-- Objetos -->
<rect x="380" y="230" width="10" height="10" fill="#f4d03f"/>
<rect x="420" y="210" width="8" height="14" fill="#8e44ad"/>
<text x="400" y="430" text-anchor="middle" font-family="monospace" font-size="14" fill="#4ecca3">PIXEL DUNGEON — Nivel B1</text>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="80" viewBox="0 0 300 80">
<rect width="300" height="80" fill="#1a1a2e" rx="6"/>
<text x="20" y="52" font-family="monospace" font-size="36" font-weight="bold" fill="#e94560">JAIL</text>
<text x="115" y="52" font-family="monospace" font-size="36" font-weight="bold" fill="#4ecca3">GAMES</text>
</svg>

After

Width:  |  Height:  |  Size: 365 B