Módulo 18: Proyecto final
Objetivos del módulo
- Planificar un sitio web de varias páginas: requisitos, estructura y wireframe
- Combinar HTML5 semántico, CSS propio moderno y Bootstrap 5.3.8 en un mismo proyecto
- Implementar navbar responsive, cards, modal, formulario validado y modo oscuro
- Auditar accesibilidad y rendimiento con Lighthouse y validar con el W3C
- Publicar el sitio en Internet con GitHub Pages
1. El proyecto: “Lumen Estudio”
Vas a construir la web de Lumen Estudio, un estudio ficticio de diseño y desarrollo web. Es el tipo de encargo que podrías recibir como primer cliente, y sirve igual como portfolio personal si cambias los textos por los tuyos.
📘 Concepto: Un proyecto real no empieza escribiendo código, sino definiendo qué hay que construir. Igual que un arquitecto dibuja planos antes de poner un ladrillo, tú definirás requisitos, estructura y bocetos.
1.1 Requisitos
| Tipo | Requisito |
|---|---|
| Páginas | Inicio, Servicios, Portfolio y Contacto |
| Navegación | Navbar común y responsive, con indicador de página actual |
| Inicio | Portada (hero) animada con llamada a la acción, resumen de servicios y proceso de trabajo |
| Servicios | Tarjetas con precios orientativos y preguntas frecuentes (acordeón) |
| Portfolio | Rejilla de proyectos; al pulsar uno se abre un modal con el detalle |
| Contacto | Formulario validado; al enviarlo correctamente se muestra un modal de confirmación |
| Tema | Modo oscuro con conmutador que recuerda la preferencia |
| Calidad | HTML válido, accesible (teclado, contraste, textos alternativos) y Lighthouse ≥ 90 |
| Publicación | Disponible en GitHub Pages |
1.2 Reparto de responsabilidades
El HTML5 semántico aporta estructura y significado; Bootstrap 5.3.8 (por CDN) pone la rejilla, las utilidades y los componentes; el CSS propio da la identidad visual (variables, hero, Grid, Flexbox, animaciones, ajustes del modo oscuro); y un poco de JavaScript gestiona el tema, la validación y el modal dinámico.
2. Estructura de carpetas
lumen-estudio/
├── index.html
├── servicios.html
├── portfolio.html
├── contacto.html
├── css/
│ └── estilos.css
├── js/
│ ├── tema.js ← se carga en el <head> (evita el parpadeo)
│ └── main.js ← lógica de la página
├── img/
│ ├── proyecto-1.webp
│ ├── proyecto-2.webp
│ └── ...
└── favicon.svg
⚠️ Importante: Usa nombres de archivo en minúsculas, sin espacios ni tildes (
proyecto-1.webp, noProyecto 1.WEBP). GitHub Pages distingue mayúsculas de minúsculas: un enlace aImg/Foto.jpgque en tu Windows funciona puede dar error 404 una vez publicado.
3. Wireframe
Antes de maquetar, haz un boceto de baja fidelidad. Así decides la estructura sin distraerte con colores.
flowchart TB
subgraph Inicio["index.html"]
direction TB
N1["Navbar: logo · Inicio · Servicios · Portfolio · Contacto · 🌓"]
H1["HERO: título grande + subtítulo + 2 botones (animado)"]
S1["SERVICIOS: 3 cards en fila (1 columna en móvil)"]
P1["PROCESO: 4 pasos en CSS Grid"]
F1["Footer: datos · enlaces · redes"]
N1 --> H1 --> S1 --> P1 --> F1
end
PORTFOLIO (escritorio) CONTACTO (escritorio)
+------------------------------------+ +------------------------------------+
| NAVBAR | | NAVBAR |
+------------------------------------+ +------------------------------------+
| Título + breadcrumb | | Título |
+-----------+-----------+------------+ +----------------------+-------------+
| [img] | [img] | [img] | | Formulario | Datos de |
| Proyecto | Proyecto | Proyecto | | nombre | email | contacto |
| [Ver] | [Ver] | [Ver] | | asunto | horario |
+-----------+-----------+------------+ | mensaje | email |
| ... | ... | ... | | [x] privacidad | |
+-----------+-----------+------------+ | [Enviar] | |
| FOOTER | +----------------------+-------------+
+------------------------------------+ | FOOTER |
+------------------------------------+
💡 Consejo: Dibuja también la versión móvil: en general, las columnas pasan a apilarse una debajo de otra. Diseñar primero el móvil (mobile first) te obliga a priorizar el contenido.
4. Página de inicio: index.html
💻 Ejemplo: Página completa. La cabecera y el pie se repiten idénticos en las demás páginas (cambiando solo el enlace activo).
<!doctype html>
<html lang="es" data-bs-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lumen Estudio · Diseño y desarrollo web</title>
<meta name="description" content="Estudio de diseño web en Valencia. Creamos webs rápidas, accesibles y responsive para pequeñas empresas.">
<link rel="icon" href="favicon.svg" type="image/svg+xml">
<!-- Tema antes de pintar: evita el destello claro en modo oscuro -->
<script src="js/tema.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;800&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css" rel="stylesheet">
<link href="css/estilos.css" rel="stylesheet">
</head>
<body>
<a class="visually-hidden-focusable" href="#contenido">Saltar al contenido principal</a>
<header>
<nav class="navbar navbar-expand-lg bg-body-tertiary border-bottom sticky-top" aria-label="Navegación principal">
<div class="container">
<a class="navbar-brand fw-bold" href="index.html">
<i class="bi bi-lightbulb-fill text-warning" aria-hidden="true"></i> Lumen
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#menu"
aria-controls="menu" aria-expanded="false" aria-label="Mostrar u ocultar el menú">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="menu">
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link active" aria-current="page" href="index.html">Inicio</a></li>
<li class="nav-item"><a class="nav-link" href="servicios.html">Servicios</a></li>
<li class="nav-item"><a class="nav-link" href="portfolio.html">Portfolio</a></li>
<li class="nav-item"><a class="nav-link" href="contacto.html">Contacto</a></li>
</ul>
<button class="btn btn-outline-secondary ms-lg-3" type="button" id="btnTema" aria-label="Cambiar a modo oscuro">
<i class="bi bi-moon-stars-fill" aria-hidden="true"></i>
</button>
</div>
</div>
</nav>
</header>
<main id="contenido">
<!-- HERO -->
<section class="hero text-center text-lg-start">
<div class="container">
<div class="row align-items-center g-5">
<div class="col-lg-7 aparecer">
<p class="text-uppercase fw-semibold hero__antetitulo mb-2">Estudio de diseño web</p>
<h1 class="display-4 fw-bold">Webs que iluminan tu negocio</h1>
<p class="lead my-4">Diseñamos y desarrollamos sitios rápidos, accesibles y preciosos en cualquier pantalla.</p>
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center justify-content-lg-start">
<a href="contacto.html" class="btn btn-primary btn-lg px-4">Pide presupuesto</a>
<a href="portfolio.html" class="btn btn-outline-light btn-lg px-4">Ver proyectos</a>
</div>
</div>
<div class="col-lg-5 d-none d-lg-block">
<div class="hero__orbe" aria-hidden="true"></div>
</div>
</div>
</div>
</section>
<!-- SERVICIOS -->
<section class="py-5" aria-labelledby="titulo-servicios">
<div class="container">
<h2 id="titulo-servicios" class="text-center mb-5">¿Qué hacemos?</h2>
<div class="row row-cols-1 row-cols-md-3 g-4">
<div class="col">
<article class="card h-100 tarjeta-servicio">
<div class="card-body">
<i class="bi bi-palette fs-1 text-primary" aria-hidden="true"></i>
<h3 class="h5 mt-3">Diseño UI/UX</h3>
<p class="card-text text-body-secondary">Interfaces claras centradas en tus usuarios.</p>
</div>
</article>
</div>
<div class="col">
<article class="card h-100 tarjeta-servicio">
<div class="card-body">
<i class="bi bi-code-slash fs-1 text-primary" aria-hidden="true"></i>
<h3 class="h5 mt-3">Desarrollo web</h3>
<p class="card-text text-body-secondary">HTML semántico, CSS moderno y código mantenible.</p>
</div>
</article>
</div>
<div class="col">
<article class="card h-100 tarjeta-servicio">
<div class="card-body">
<i class="bi bi-speedometer2 fs-1 text-primary" aria-hidden="true"></i>
<h3 class="h5 mt-3">Rendimiento y SEO</h3>
<p class="card-text text-body-secondary">Webs rápidas que Google y tus clientes adoran.</p>
</div>
</article>
</div>
</div>
</div>
</section>
<!-- PROCESO (CSS Grid propio) -->
<section class="py-5 bg-body-tertiary" aria-labelledby="titulo-proceso">
<div class="container">
<h2 id="titulo-proceso" class="text-center mb-5">Cómo trabajamos</h2>
<ol class="proceso">
<li class="proceso__paso"><span class="proceso__num">1</span><h3 class="h6">Escuchamos</h3><p>Reunión para entender tu proyecto.</p></li>
<li class="proceso__paso"><span class="proceso__num">2</span><h3 class="h6">Diseñamos</h3><p>Wireframes y prototipo visual.</p></li>
<li class="proceso__paso"><span class="proceso__num">3</span><h3 class="h6">Desarrollamos</h3><p>Maquetación responsive y pruebas.</p></li>
<li class="proceso__paso"><span class="proceso__num">4</span><h3 class="h6">Publicamos</h3><p>Lanzamiento y formación.</p></li>
</ol>
</div>
</section>
</main>
<footer class="pie py-4 border-top">
<div class="container pie__contenido">
<p class="mb-0">© <span id="anio">2026</span> Lumen Estudio · Valencia</p>
<ul class="list-unstyled d-flex gap-3 mb-0 fs-5">
<li><a href="https://github.com/" class="link-body-emphasis" aria-label="GitHub"><i class="bi bi-github" aria-hidden="true"></i></a></li>
<li><a href="https://www.linkedin.com/" class="link-body-emphasis" aria-label="LinkedIn"><i class="bi bi-linkedin" aria-hidden="true"></i></a></li>
</ul>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
<script src="js/main.js"></script>
</body>
</html>
💡 Consejo: En las demás páginas, copia el
<head>, el<header>y el<footer>, cambia el<title>y ladescription, y mueveactiveyaria-current="page"al enlace correspondiente.
5. Hoja de estilos propia: css/estilos.css
/* =========================================================
1. Variables: identidad de marca
========================================================= */
:root {
--lumen-acento: #6f42c1;
--lumen-acento-2: #ff9f1c;
--lumen-hero-fondo: linear-gradient(135deg, #1e1b4b 0%, #4c1d95 55%, #6f42c1 100%);
--lumen-radio: 1rem;
--lumen-sombra: 0 0.75rem 2rem rgb(0 0 0 / 0.08);
/* Integración con Bootstrap: fuente y color primario */
--bs-body-font-family: "Outfit", system-ui, sans-serif;
--bs-primary: var(--lumen-acento);
--bs-primary-rgb: 111, 66, 193;
--bs-link-color-rgb: 111, 66, 193;
}
[data-bs-theme="dark"] {
--lumen-hero-fondo: linear-gradient(135deg, #0b0a1f 0%, #1e1b4b 60%, #3b1f7a 100%);
--lumen-sombra: 0 0.75rem 2rem rgb(0 0 0 / 0.4);
--bs-body-bg: #0f0e1a;
--bs-tertiary-bg: #17152a;
--bs-link-color-rgb: 177, 151, 252;
}
/* Botón primario con el color de marca (variables de componente) */
.btn-primary {
--bs-btn-bg: var(--lumen-acento);
--bs-btn-border-color: var(--lumen-acento);
--bs-btn-hover-bg: color-mix(in srgb, var(--lumen-acento), black 15%);
--bs-btn-hover-border-color: color-mix(in srgb, var(--lumen-acento), black 15%);
--bs-btn-active-bg: color-mix(in srgb, var(--lumen-acento), black 25%);
--bs-btn-focus-shadow-rgb: 111, 66, 193;
}
h1, h2, h3 { text-wrap: balance; }
p { text-wrap: pretty; }
:focus-visible {
outline: 3px solid var(--lumen-acento-2);
outline-offset: 3px;
}
/* =========================================================
2. Hero
========================================================= */
.hero {
background: var(--lumen-hero-fondo);
color: #fff;
padding-block: clamp(4rem, 10vw, 8rem);
.hero__antetitulo { color: var(--lumen-acento-2); letter-spacing: 0.1em; }
}
.hero__orbe {
aspect-ratio: 1;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, #ffd166, var(--lumen-acento-2) 40%, transparent 70%);
animation: flotar 6s ease-in-out infinite;
}
/* =========================================================
3. Tarjetas de servicio
========================================================= */
.tarjeta-servicio {
border: none;
border-radius: var(--lumen-radio);
box-shadow: var(--lumen-sombra);
transition: transform 0.25s ease, box-shadow 0.25s ease;
&:hover { transform: translateY(-6px); }
}
/* =========================================================
4. Proceso: CSS Grid propio
========================================================= */
.proceso {
list-style: none;
padding: 0;
margin: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
gap: 1.5rem;
}
.proceso__paso {
background: var(--bs-body-bg);
border-radius: var(--lumen-radio);
padding: 1.5rem;
box-shadow: var(--lumen-sombra);
p { margin: 0; color: var(--bs-secondary-color); }
}
.proceso__num {
display: grid;
place-items: center;
inline-size: 2.5rem;
block-size: 2.5rem;
margin-block-end: 0.75rem;
border-radius: 50%;
background: var(--lumen-acento);
color: #fff;
font-weight: 800;
}
/* =========================================================
5. Portfolio
========================================================= */
.proyecto {
border-radius: var(--lumen-radio);
overflow: hidden;
img {
aspect-ratio: 4 / 3;
object-fit: cover;
transition: transform 0.4s ease;
}
&:hover img,
&:focus-within img { transform: scale(1.05); }
}
/* =========================================================
6. Pie con Flexbox
========================================================= */
.pie__contenido {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
/* =========================================================
7. Animaciones (respetando la preferencia de movimiento)
========================================================= */
@keyframes aparecer {
from { opacity: 0; transform: translateY(1.5rem); }
to { opacity: 1; transform: none; }
}
@keyframes flotar {
50% { transform: translateY(-1rem); }
}
.aparecer { animation: aparecer 0.8s ease-out both; }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
💡 Consejo: Repasa el fichero buscando lo aprendido en el curso: variables (propias y
--bs-*),color-mix(),clamp(),min(),text-wrap, anidamiento, Grid conauto-fit/minmax(), Flexbox, propiedades lógicas y@keyframesconprefers-reduced-motion.
6. JavaScript
6.1 js/tema.js (en el <head>)
// Se ejecuta antes de pintar la página para aplicar el tema guardado
(() => {
const guardado = localStorage.getItem('tema');
const prefiereOscuro = window.matchMedia('(prefers-color-scheme: dark)').matches;
const tema = guardado ?? (prefiereOscuro ? 'dark' : 'light');
document.documentElement.setAttribute('data-bs-theme', tema);
})();
6.2 js/main.js (al final del <body>)
// ---------- 1. Conmutador de tema ----------
const btnTema = document.getElementById('btnTema');
function pintarBotonTema() {
const oscuro = document.documentElement.getAttribute('data-bs-theme') === 'dark';
btnTema.innerHTML = oscuro
? '<i class="bi bi-sun-fill" aria-hidden="true"></i>'
: '<i class="bi bi-moon-stars-fill" aria-hidden="true"></i>';
btnTema.setAttribute('aria-label', oscuro ? 'Cambiar a modo claro' : 'Cambiar a modo oscuro');
}
btnTema?.addEventListener('click', () => {
const actual = document.documentElement.getAttribute('data-bs-theme');
const nuevo = actual === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-bs-theme', nuevo);
localStorage.setItem('tema', nuevo);
pintarBotonTema();
});
if (btnTema) pintarBotonTema();
// ---------- 2. Año actual en el pie ----------
const anio = document.getElementById('anio');
if (anio) anio.textContent = new Date().getFullYear();
// ---------- 3. Validación del formulario de contacto ----------
const form = document.getElementById('formContacto');
form?.addEventListener('submit', (evento) => {
evento.preventDefault(); // en este proyecto no hay servidor
form.classList.add('was-validated');
if (!form.checkValidity()) {
form.querySelector(':invalid')?.focus(); // llevar el foco al primer error
return;
}
const nombre = form.elements.nombre.value.trim();
document.getElementById('nombreConfirmacion').textContent = nombre;
bootstrap.Modal.getOrCreateInstance(document.getElementById('modalGracias')).show();
form.reset();
form.classList.remove('was-validated');
});
// ---------- 4. Modal dinámico del portfolio ----------
const modalProyecto = document.getElementById('modalProyecto');
modalProyecto?.addEventListener('show.bs.modal', (evento) => {
const boton = evento.relatedTarget; // botón que abrió el modal
modalProyecto.querySelector('.modal-title').textContent = boton.dataset.titulo;
modalProyecto.querySelector('.modal-body p').textContent = boton.dataset.descripcion;
const img = modalProyecto.querySelector('.modal-body img');
img.src = boton.dataset.imagen;
img.alt = boton.dataset.alt;
});
📘 Concepto: Un único
main.jssirve para todas las páginas gracias al operador?.(encadenamiento opcional): si en una página no existe el formulario o el modal, el código simplemente no hace nada en lugar de producir un error.
7. Página de portfolio (contenido del <main>)
<main id="contenido" class="py-5">
<div class="container">
<nav aria-label="Ruta de navegación">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="index.html">Inicio</a></li>
<li class="breadcrumb-item active" aria-current="page">Portfolio</li>
</ol>
</nav>
<h1 class="mb-4">Proyectos recientes</h1>
<div class="row row-cols-1 row-cols-sm-2 row-cols-lg-3 g-4">
<div class="col">
<article class="card h-100 proyecto">
<img src="img/proyecto-1.webp" class="card-img-top" width="800" height="600" loading="lazy"
alt="Web de la panadería La Espiga en un portátil">
<div class="card-body d-flex flex-column">
<h2 class="h5 card-title">Panadería La Espiga</h2>
<p class="card-text text-body-secondary">Web corporativa con pedidos por WhatsApp.</p>
<button type="button" class="btn btn-outline-primary mt-auto"
data-bs-toggle="modal" data-bs-target="#modalProyecto"
data-titulo="Panadería La Espiga"
data-descripcion="Rediseño completo con catálogo de productos y botón de pedido. +40% de pedidos en tres meses."
data-imagen="img/proyecto-1.webp"
data-alt="Web de la panadería La Espiga en un portátil">
Ver detalle <span class="visually-hidden">de Panadería La Espiga</span>
</button>
</div>
</article>
</div>
<!-- Repite el bloque .col para cada proyecto, cambiando los data-* -->
</div>
</div>
<!-- Modal reutilizable: su contenido lo rellena main.js -->
<div class="modal fade" id="modalProyecto" tabindex="-1" aria-labelledby="modalProyectoTitulo" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title fs-5" id="modalProyectoTitulo">Proyecto</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal-body">
<img src="" class="img-fluid rounded mb-3" alt="">
<p class="mb-0"></p>
</div>
</div>
</div>
</div>
</main>
💡 Consejo: Indica siempre
widthyheighten las imágenes (aunque luego CSS las haga responsive): el navegador reserva el espacio y evitas saltos de contenido (CLS), una métrica que Lighthouse penaliza.loading="lazy"retrasa la carga de las imágenes que no están en pantalla.
8. Página de contacto (contenido del <main>)
<main id="contenido" class="py-5">
<div class="container">
<h1 class="mb-4">Contacto</h1>
<div class="row g-5">
<div class="col-lg-8">
<form id="formContacto" class="row g-3" novalidate>
<div class="col-md-6">
<label for="nombre" class="form-label">Nombre</label>
<input type="text" class="form-control" id="nombre" name="nombre" autocomplete="name" required>
<div class="invalid-feedback">Escribe tu nombre.</div>
</div>
<div class="col-md-6">
<label for="email" class="form-label">Correo electrónico</label>
<input type="email" class="form-control" id="email" name="email" autocomplete="email" required>
<div class="invalid-feedback">Introduce un correo válido.</div>
</div>
<div class="col-12">
<label for="asunto" class="form-label">¿Qué necesitas?</label>
<select class="form-select" id="asunto" name="asunto" required>
<option value="" selected disabled>Elige una opción</option>
<option>Web corporativa</option>
<option>Tienda online</option>
<option>Rediseño</option>
</select>
<div class="invalid-feedback">Selecciona una opción.</div>
</div>
<div class="col-12">
<div class="form-floating">
<textarea class="form-control" id="mensaje" name="mensaje" placeholder="Mensaje"
style="height: 9rem" minlength="20" required></textarea>
<label for="mensaje">Cuéntanos tu proyecto</label>
<div class="invalid-feedback">Escribe al menos 20 caracteres.</div>
</div>
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="privacidad" required>
<label class="form-check-label" for="privacidad">He leído y acepto la política de privacidad</label>
<div class="invalid-feedback">Debes aceptarla para enviar el mensaje.</div>
</div>
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-send me-1" aria-hidden="true"></i> Enviar
</button>
</div>
</form>
</div>
<aside class="col-lg-4">
<address class="p-4 bg-body-tertiary rounded-4 mb-0">
<h2 class="h5">Lumen Estudio</h2>
<p><i class="bi bi-geo-alt me-2" aria-hidden="true"></i>Calle de la Luz, 10 · Valencia</p>
<p><i class="bi bi-envelope me-2" aria-hidden="true"></i><a href="mailto:hola@lumen.example">hola@lumen.example</a></p>
<p class="mb-0"><i class="bi bi-clock me-2" aria-hidden="true"></i>L-V de 9:00 a 18:00</p>
</address>
</aside>
</div>
</div>
<!-- Modal de confirmación -->
<div class="modal fade" id="modalGracias" tabindex="-1" aria-labelledby="modalGraciasTitulo" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content text-center p-3">
<div class="modal-body">
<i class="bi bi-check-circle-fill text-success display-4" aria-hidden="true"></i>
<h2 class="h4 mt-3" id="modalGraciasTitulo">¡Gracias, <span id="nombreConfirmacion"></span>!</h2>
<p>Hemos recibido tu mensaje. Te responderemos en menos de 24 horas.</p>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
</main>
⚠️ Importante: GitHub Pages solo sirve ficheros estáticos: no puede procesar formularios. Para recibir los mensajes de verdad necesitarías un servicio externo de formularios o un backend (por ejemplo, en PHP). En este proyecto simulamos el envío con JavaScript.
La página Servicios sigue el mismo patrón: un <h1>, una fila row-cols-1 row-cols-md-3 de cards con precio (card-header, list-group-flush con lo incluido, card-footer con botón) y un acordeón de preguntas frecuentes como el del módulo 16.
9. Checklist de calidad
9.1 Accesibilidad
| ✔ | Comprobación |
|---|---|
| ☐ | lang="es" en <html> y un <title> distinto en cada página |
| ☐ | Un solo <h1> por página y jerarquía de títulos sin saltos (h1 → h2 → h3) |
| ☐ | Todas las imágenes con alt descriptivo (o alt="" si son decorativas) |
| ☐ | Iconos decorativos con aria-hidden="true"; botones de solo icono con aria-label |
| ☐ | Se puede usar toda la web solo con el teclado (Tab, Shift+Tab, Enter, Esc) y el foco es visible |
| ☐ | Enlace “Saltar al contenido” funcional |
| ☐ | Contraste suficiente en ambos modos (claro y oscuro) |
| ☐ | Cada campo del formulario tiene su <label> y los errores se explican con texto |
| ☐ | Las animaciones se reducen con prefers-reduced-motion |
9.2 Rendimiento
| ✔ | Comprobación |
|---|---|
| ☐ | Imágenes en formato moderno (WebP/AVIF), con el tamaño adecuado y width/height |
| ☐ | loading="lazy" en imágenes fuera de la primera pantalla (nunca en la del hero) |
| ☐ | Fuentes con preconnect y display=swap; como máximo 2 familias |
| ☐ | Scripts al final del <body> (o con defer) |
| ☐ | Sin CSS ni JS que no se use |
9.3 Auditoría con Lighthouse
- Abre tu página en Chrome o Edge y pulsa
F12. - Ve a la pestaña Lighthouse.
- Elige modo Navigation, dispositivo Mobile y marca Performance, Accessibility, Best Practices y SEO.
- Pulsa Analyze page load y espera el informe.
- Revisa cada aviso: Lighthouse explica el problema y enlaza a cómo solucionarlo.
💡 Consejo: Haz la auditoría en una ventana de incógnito (las extensiones del navegador alteran los resultados) y repítela con la web ya publicada en GitHub Pages, que es lo que verán tus usuarios. Objetivo: 90 o más en las cuatro categorías.
9.4 Validación W3C
| Validador | Dirección | Qué comprueba |
|---|---|---|
| HTML | validator.w3.org | Etiquetas mal cerradas, atributos inválidos, id duplicados… |
| CSS | jigsaw.w3.org/css-validator | Propiedades y valores incorrectos |
Puedes validar por URL (una vez publicada), subiendo el fichero o pegando el código. Corrige todos los errores; los avisos (warnings) revísalos con criterio.
⚠️ Importante: El validador de CSS puede marcar como error alguna característica muy reciente (anidamiento,
@layer,color-mix()…) aunque los navegadores la soporten. Si estás seguro de que es CSS válido y actual (compruébalo en MDN), puedes ignorar ese aviso concreto.
10. Publicación en GitHub Pages
📘 Concepto: GitHub Pages es un servicio gratuito de GitHub que publica como sitio web los ficheros de un repositorio. Tu web quedará en
https://TU-USUARIO.github.io/NOMBRE-REPOSITORIO/.
10.1 Crear el repositorio
- Crea una cuenta en github.com (si no la tienes).
- Pulsa New repository (botón + arriba a la derecha).
- Nombre:
lumen-estudio. Visibilidad: Public. No marques ninguna opción de inicialización. - Pulsa Create repository.
10.2 Subir el proyecto con Git
Abre la terminal de VS Code en la carpeta del proyecto:
# (Si es tu primera vez con Git, configura antes user.name y user.email)
# Convertir la carpeta en repositorio y hacer el primer commit
git init
git add .
git commit -m "Primera versión de la web de Lumen Estudio"
# Conectar con GitHub y subir
git branch -M main
git remote add origin https://github.com/TU-USUARIO/lumen-estudio.git
git push -u origin main
💡 Consejo: Si no quieres usar la terminal todavía, en la página del repositorio vacío tienes el enlace uploading an existing file: arrastra tus ficheros y carpetas y pulsa Commit changes.
10.3 Activar GitHub Pages
- En el repositorio, ve a Settings → Pages (menú lateral).
- En Build and deployment, Source: Deploy from a branch.
- Branch: main y carpeta / (root). Pulsa Save.
- Espera uno o dos minutos y recarga: aparecerá “Your site is live at…” con la URL.
10.4 Actualizar la web
Cada cambio que subas (git add ., git commit -m "Describe el cambio" y git push) se publica automáticamente en uno o dos minutos.
| Problema tras publicar | Causa habitual | Solución |
|---|---|---|
| Error 404 en la página principal | No hay index.html en la raíz | El fichero de inicio debe llamarse exactamente index.html |
| Imágenes o CSS no cargan | Rutas absolutas (/css/...) o mayúsculas distintas | Usa rutas relativas (css/estilos.css) y respeta mayúsculas |
| Los cambios no aparecen | Caché del navegador o despliegue en curso | Espera, revisa la pestaña Actions y recarga con Ctrl + F5 |
Una carpeta que empieza por _ no se publica | GitHub Pages usa Jekyll por defecto | Crea un fichero vacío .nojekyll en la raíz |
11. Propuestas de ampliación
| Ampliación | Qué practicarás |
|---|---|
Filtro del portfolio por categorías (botones btn-check) | JavaScript y atributos data-* |
| Página de blog con artículos y paginación | Más HTML semántico (article, time) |
View transitions entre páginas (@view-transition) | CSS moderno (módulo 14) |
| Compilar Bootstrap con Sass solo con los componentes usados | Personalización avanzada (módulo 17) |
| Formulario real con un servicio externo o un backend propio | Integración cliente-servidor |
Versión en inglés con hreflang y dominio propio en GitHub Pages | Internacionalización, SEO y DNS |
12. Ejercicios
Ejercicio 1: Planificación y página de inicio
Escribe los requisitos de tu versión del proyecto (tu portfolio o la web de un negocio real o inventado) y dibuja el wireframe de las cuatro páginas en escritorio y móvil. Después crea la estructura de carpetas y la página index.html completa con navbar, hero animado, servicios, proceso y pie. Valida el HTML en el W3C hasta obtener cero errores.
Ejercicio 2: Servicios con precios
Construye servicios.html con tres cards de precios (la central destacada con border-primary y un badge “Más popular”) y un acordeón con al menos cuatro preguntas frecuentes.
Ejercicio 3: Portfolio con modal
Crea portfolio.html con al menos seis proyectos y el modal reutilizable. Comprueba con el teclado que el foco entra en el modal al abrirlo y vuelve al botón al cerrarlo.
Ejercicio 4: Contacto validado
Implementa contacto.html con el formulario validado y el modal de agradecimiento. Añade un campo de teléfono opcional con pattern y un mensaje de error propio.
Ejercicio 5: Auditoría y mejora
Pasa Lighthouse en modo móvil a las cuatro páginas, en modo claro y oscuro. Anota las puntuaciones iniciales, corrige los problemas y vuelve a medir hasta llegar a 90 o más en todas las categorías.
Ejercicio 6: Publicación
Publica el proyecto en GitHub Pages, comparte la URL y añade al repositorio un fichero README.md que explique el proyecto, las tecnologías usadas y el enlace a la web publicada.
Resumen
| Fase | Herramientas y técnicas |
|---|---|
| Planificación | Requisitos, estructura de carpetas, wireframe (Mermaid / texto) |
| Estructura | HTML5 semántico, skip link, jerarquía de títulos, alt |
| Maquetación | Rejilla y utilidades de Bootstrap + CSS Grid y Flexbox propios |
| Identidad visual | Variables CSS, variables --bs-*, color-mix(), clamp(), anidamiento |
| Componentes | Navbar, cards, breadcrumb, modal, acordeón, formularios de Bootstrap |
| Interactividad | data-bs-theme + localStorage, validación con .was-validated, evento show.bs.modal |
| Animación | @keyframes, transition, prefers-reduced-motion |
| Calidad | Checklist de accesibilidad y rendimiento, Lighthouse, validadores W3C |
| Publicación | Git, GitHub, Settings → Pages → Deploy from a branch |
¡Enhorabuena! Has completado el curso. Ahora tienes las bases para maquetar cualquier sitio web moderno, accesible y responsive. El siguiente paso natural es profundizar en JavaScript para dar vida a tus interfaces.