Este tutorial adapta pedagógicamente el Día 59, que abre el bloque avanzado de aplicaciones web (Días 59 a 70). Las secciones 📝 Ejercicio original del curso provienen de las lecciones 417 a 424; las soluciones oficiales están en los gists enlazados en cada paso. Los retos 🟢🟡🟠🔴, la comparación entre
includey herencia de plantillas, el proyecto extra y el proyecto integrador del bloque son creaciones de esta adaptación. Las correcciones al material original aparecen como 🛠️ Nota técnica.
Nota sobre las verificaciones: el 27 de septiembre de 2026, con Python 3.14, Flask 3.1.3 y Jinja 3.1.6, se descargó la plantilla Clean Blog de Start Bootstrap (versión 6.0.9, basada en Bootstrap 5.2.3) y se revisó su estructura de carpetas y sus rutas. Se consultó la API de ejemplo del curso (
api.npoint.io/674f5423f73deab1e9a7): devuelve 3 entradas conid,title,subtitle,bodyeimage_url(las tres imágenes responden), sinauthornidate. Se ejecutaron con el cliente de pruebas de Flask los ejemplos deinclude.
Tabla de contenidos
- Antes de empezar: mejorar el blog sin escribir Bootstrap
- Pasos 1 y 2: el proyecto de inicio y la portada
- Paso 3: arreglar el encabezado y el pie de página
- Paso 4:
{% include %}para no repetir código - Paso 5: las páginas "Acerca de" y "Contacto"
- Pasos 6 y 7: entradas desde una API y páginas individuales
- 🚀 Proyecto del día: el blog Clean Blog completo
- 🌟 Proyecto extra: el periódico escolar digital
- 🏗️ Proyecto integrador del bloque avanzado — Etapa 1
- 🧩 Comprueba lo aprendido
- 🤖 Programar con apoyo de IA
- 🛠️ Correcciones técnicas al material original
- 📋 Matriz de trazabilidad
- 📦 Evidencias de aprendizaje del Día 59
0. Antes de empezar: mejorar el blog sin escribir Bootstrap
(Fuente: 417-day-59-goals-what-you-ll-make-by-the-end-of-today.md)
📝 Contenido original del curso
El blog del Día 57 tenía un CSS sencillo, sin animaciones y no era responsivo. Ahora que conoces Bootstrap (Día 58), lo vas a mejorar… "y lo mejor: ni siquiera tienes que escribir el código de Bootstrap". En Internet hay cientos de miles de plantillas Bootstrap gratuitas; basta con entender cómo funciona Bootstrap para personalizarlas. Ejemplos: bootstrapmade.com, los ejemplos de getbootstrap.com y los temas gratuitos de creative-tim.com.
Al final del día tendrás un blog con:
- varias páginas y una barra de navegación interactiva;
- páginas de entradas generadas dinámicamente, con títulos a pantalla completa;
- diseño totalmente responsivo con una barra de navegación que se adapta al celular.
Prerequisitos: plantillas, estáticos y
url_for(Días 56 y 57), Bootstrap (Día 58), el blog del Día 57 (su ruta/post/<int:index>).
📖 Lo nuevo de hoy (adaptación)
Casi todo lo de hoy ya lo sabes hacer. Lo nuevo es una sola idea: dividir las páginas en piezas reutilizables (encabezado, pie de página) con {% include %}. Es la misma idea de las funciones en Python: escribir algo una vez y usarlo muchas veces (principio DRY, Don't Repeat Yourself).
1. Pasos 1 y 2: el proyecto de inicio y la portada
(Fuentes: 418-step-1-download-the-starting-project.md y 419-step-2-get-the-home-page-to-work.md)
📝 Ejercicio original del curso: pasos 1 y 2
Paso 1.
- Descarga la plantilla Clean Blog de Start Bootstrap:
https://startbootstrap.com/previews/clean-blog/. - Descomprime el archivo y renombra la carpeta a "upgraded blog".
- Ábrela en PyCharm y:
- A. Crea las carpetas
staticytemplates. - B. Mueve los archivos HTML a
templatesy todas las carpetas astatic. - C. Crea
header.html,footer.htmly el importantísimomain.py. - D. Comprueba que la estructura coincide con la imagen del curso.
- A. Crea las carpetas
Paso 2. Con lo que sabes de Flask, haz que la portada se muestre en http://localhost:5000. "No te preocupes todavía por el estilo; lo arreglamos en el siguiente paso".
📖 La estructura que debes obtener (verificada con Clean Blog 6.0.9)
La descarga trae una carpeta dist con el sitio listo:
upgraded blog/
├── main.py
├── templates/
│ ├── index.html about.html post.html contact.html
│ ├── header.html ← nuevo (vacío por ahora)
│ └── footer.html ← nuevo (vacío por ahora)
└── static/
├── assets/
│ ├── favicon.ico
│ └── img/ (home-bg.jpg, about-bg.jpg, contact-bg.jpg, post-bg.jpg, post-sample-image.jpg)
├── css/styles.css
└── js/scripts.jsfrom flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def home():
return render_template("index.html")
if __name__ == "__main__":
app.run(debug=True)🛠️ Nota técnica: en la plantilla actual las imágenes están en
assets/img/. Si ves capturas o soluciones antiguas conimg/directamente en la raíz, es de una versión anterior de Clean Blog; ajusta las rutas a la versión que descargaste.
⚠️ Errores frecuentes
| Error | Causa | Solución |
|---|---|---|
TemplateNotFound: index.html |
Los HTML quedaron dentro de templates/dist/ |
Muévelos directamente a templates/ |
| Carpeta con espacio en el nombre ("upgraded blog") y la terminal falla | El espacio separa argumentos | Comillas en la terminal: cd "upgraded blog" (Día 54) |
| La portada sale sin estilos | Es lo esperado en el paso 2 | Paso 3 |
2. Paso 3: arreglar el encabezado y el pie de página
(Fuente: 420-step-3-fix-the-header-and-footer.md)
🎯 Objetivo de aprendizaje
Corregir todas las rutas a recursos estáticos de una plantilla ajena usando url_for, incluidas las que están dentro de un atributo style.
📝 Ejercicio original del curso: paso 3
Los estilos no aparecen porque los archivos estáticos ahora los sirve el servidor y ya no están donde el HTML los busca.
- Arregla el
<head>deindex.htmlpara que aparezcan los estilos y Bootstrap. Nota del curso: "suele ser buena idea construir una URL dinámica para los recursos estáticos en lugar de apuntar a la carpetastatic" (documentación de Flask sobre archivos estáticos). - Arregla los recursos del pie de página para que funcione el JavaScript: la barra de navegación desaparece al bajar y reaparece al subir.
- Haz que reaparezca la imagen de fondo
home-bg.jpgcorrigiendo la sección<header>.
📖 Explicación conceptual
Hay tres tipos de rutas por corregir (verificado en index.html):
| Dónde | Original | Corregido |
|---|---|---|
<head> |
<link href="css/styles.css" rel="stylesheet"> |
<link href="{{ url_for('static', filename='css/styles.css') }}" rel="stylesheet"> |
| Favicon | href="assets/favicon.ico" |
href="{{ url_for('static', filename='assets/favicon.ico') }}" |
Antes de </body> |
<script src="js/scripts.js"> |
<script src="{{ url_for('static', filename='js/scripts.js') }}"> |
<header> |
style="background-image: url('assets/img/home-bg.jpg')" |
style="background-image: url('{{ url_for('static', filename='assets/img/home-bg.jpg') }}')" |
Las rutas que empiezan por https:// (Bootstrap en jsDelivr, Font Awesome, Google Fonts) no se tocan: vienen de Internet.
El caso difícil: el atributo style. Jinja reemplaza {{ ... }} en cualquier parte del archivo, incluso dentro de un atributo o de CSS en línea. Cuidado con las comillas: el atributo usa dobles, url(...) usa simples, y dentro de url_for también van simples. Jinja procesa el archivo antes de que el navegador lo lea, así que no hay conflicto: al navegador le llega url('/static/assets/img/home-bg.jpg').
⚠️ Errores frecuentes
| Error | Causa | Solución |
|---|---|---|
| El CSS carga, pero la barra no se oculta al bajar | Falta corregir js/scripts.js |
Corrige el <script> del final |
Aparece el texto {{ url_for(...) }} en la página |
Abriste el HTML con doble clic | Visítalo a través de Flask |
| La imagen del encabezado no aparece | Ruta relativa en el style |
url_for dentro del url(...) |
BuildError |
Escribiste url_for('statics', ...) o file= |
Es url_for('static', filename=...) |
3. Paso 4: {% include %} para no repetir código
(Fuente: 421-step-4-using-jinja-include-for-render-templates.md)
🎯 Objetivo de aprendizaje
Separar el encabezado y el pie de página en archivos propios e insertarlos en cada página con {% include %}.
📝 Ejercicio original del curso: paso 4
Las plantillas de Jinja se llaman así porque permiten reutilizar partes: en lugar de reescribir el encabezado, la navegación y el pie en cada página, se crean una vez y se aplican a todas:
{% include "header.html" %}
Parte que cambia en cada página (por ejemplo, el cuerpo).
{% include "footer.html" %}Al renderizar, el contenido de header.html y footer.html se inserta donde está cada include (documentación de Jinja, sección Include).
- Quita de
index.htmlel<head>y la navegación, y ponlos enheader.html. - Quita el
<footer>deindex.htmly ponlo enfooter.html. - Usa
includepara que el sitio funcione exactamente igual que antes.
📖 ¿Dónde se corta? (adaptación)
index.html original después del paso 4
┌──────────────────────────────┐
│ <!DOCTYPE html> <html> │ ┐
│ <head> ... </head> │ │
│ <body> │ ├─▶ header.html
│ <nav> ... </nav> │ ┘
│ <header> imagen y título │ ┐
│ <div class="container"> │ ├─▶ se queda en index.html
│ entradas ... │ ┘
│ <footer> ... </footer> │ ┐
│ <script> ... </script> │ ├─▶ footer.html
│ </body> </html> │ ┘
└──────────────────────────────┘Por qué el <header> con la imagen se queda en cada página: cada página tiene una imagen y un título distintos (home-bg.jpg, about-bg.jpg, contact-bg.jpg, post-bg.jpg). Solo sale a header.html lo que es igual en todas.
header.html termina "abierto" (con <body> sin cerrar) y footer.html empieza "a mitad de camino" (cierra </body></html>). Ninguno de los dos es HTML válido por sí solo; lo es la página ya ensamblada.
📖 Alternativa: herencia de plantillas (adaptación, la verás el Día 61)
{% include %} (hoy) |
{% extends %} + {% block %} (Día 61) |
|---|---|
| Cada página incluye las piezas comunes | Una plantilla base define la estructura y cada página rellena huecos |
| Fácil de entender | Más ordenado en sitios grandes; permite cambiar el <title> por página |
| Las piezas no son HTML válido por separado | La base es un HTML completo |
Ambas son correctas; el curso usa include hoy y herencia más adelante.
⚠️ Errores frecuentes
| Error | Causa | Solución |
|---|---|---|
TemplateNotFound: header.html |
Comillas o nombre distinto (Header.html) |
Nombre exacto y entre comillas: {% include "header.html" %} |
| La barra aparece dos veces | No la borraste de index.html |
Quita de la página todo lo que pasó a header.html |
| El JavaScript dejó de funcionar | El <script> quedó fuera de footer.html o antes de Bootstrap |
Orden: Bootstrap primero, scripts.js después |
| Estilos rotos en todas las páginas a la vez | Error en header.html |
¡Ventaja del include! Se corrige una vez |
4. Paso 5: las páginas "Acerca de" y "Contacto"
(Fuente: 422-step-5-make-the-about-and-contact-pages-work.md)
📝 Ejercicio original del curso: paso 5
- En
header.html, borra el enlace de la navegación que apunta a "Sample Post". - Cambia los
hrefconurl_forpara que la navegación lleve a "About" y "Contact". - Actualiza
main.pypara que esas rutas existan. - Actualiza
about.htmlycontact.htmlpara que usen el encabezado y el pie. - Haz que aparezcan las imágenes de fondo de "About" y "Contact". Pista: mira el atributo
styledel<header>; las imágenes son archivos estáticos.
📖 Explicación conceptual
@app.route("/about")
def about():
return render_template("about.html")
@app.route("/contact")
def contact():
return render_template("contact.html")<!-- header.html -->
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="{{ url_for('home') }}">Home</a></li>
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="{{ url_for('about') }}">About</a></li>
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="{{ url_for('contact') }}">Contact</a></li>Como header.html se incluye en todas las páginas, basta con corregir los enlaces una vez.
🛠️ Nota técnica (verificado): el formulario de
contact.htmlde la plantilla usa SB Forms, un servicio de Start Bootstrap (data-sb-form-api-token="API_TOKEN"y el scriptsb-forms-latest.js). Sin una clave de ese servicio, el botón queda deshabilitado o el envío no llega a ninguna parte. Hoy solo haz que la página se vea; el Día 60 conectarás el formulario a Flask (el curso entrega uncontact.htmlsimplificado para eso).
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1 — Aplicación: cambia los textos de "About" por una presentación tuya (sin datos sensibles, Día 56).
- 🟡 Reto 2 — Ampliación: marca como activo el enlace de la página actual. Pista:
request.endpointestá disponible en las plantillas:{% if request.endpoint == 'about' %}active{% endif %}. - 🟠 Reto 3 — Desafío: reemplaza la imagen de fondo de cada página por una propia de Unsplash y lleva un registro en
CREDITOS.md. - 🔴 Reto 4 — Pensamiento computacional: ¿qué cambiarías si el sitio tuviera 30 páginas con imágenes de encabezado distintas? Diseña una solución en la que cada ruta envíe a la plantilla el nombre de la imagen y el título (pista: variables en
render_templatey un solo bloque<header>en un archivo incluido).
5. Pasos 6 y 7: entradas desde una API y páginas individuales
(Fuentes: 423-step-6-fetch-and-render-the-blog-posts-from-an-api.md y 424-step-7-rendering-individual-posts.md)
📝 Ejercicio original del curso: paso 6
Como en el blog anterior, las entradas vienen de una API en n:point:
- Crea tu propio "JSON bin" en npoint.io con los datos de ejemplo del curso. Ejemplo de documento:
https://www.npoint.io/docs/674f5423f73deab1e9a7; tu dirección será algo comohttps://api.npoint.io/674f5423f73deab1e9a7. - En
main.py, obtén los datos de la API. - En la portada, reemplaza el
title, elsubtitle, elauthory ladatede cada entrada por los de la API. "Agrega los campos que hagan falta a tu documento".
- Pista 1: no necesitas una clase propia: en Jinja puedes usar la notación de punto (
post.title) en lugar de corchetes. - Pista 2: usa un ciclo
foren la plantilla.
La imagen de fondo del ejemplo es una foto de Unsplash; puedes buscar la tuya.
📝 Ejercicio original del curso: paso 7
Al hacer clic en el título de una entrada en la portada, debe abrirse post.html con el título, subtítulo, imagen, fecha, autor y cuerpo de esa entrada. "Ya lo hiciste en el blog anterior. Si no lo recuerdas, mira el código anterior y adáptalo. Programar es experimentar, no es un examen: no tienes que memorizarlo todo".
📖 Explicación conceptual
Los datos de ejemplo no traen autor ni fecha (verificado). La API del curso tiene 3 entradas con id, title, subtitle, body e image_url. Por eso el curso dice "agrega los campos que hagan falta": en tu bin agrega "author" y "date" a cada entrada. Si usas la API del curso tal cual, {{ post.author }} sale vacío sin ningún error (Jinja no avisa de claves que no existen).
import requests
URL_POSTS = "https://api.npoint.io/TU_BIN" # tu propio documento
posts = requests.get(URL_POSTS, timeout=10).json()
@app.route("/")
def home():
return render_template("index.html", all_posts=posts)
@app.route("/post/<int:index>")
def show_post(index):
requested_post = None
for blog_post in posts:
if blog_post["id"] == index:
requested_post = blog_post
return render_template("post.html", post=requested_post)<!-- index.html: una vista previa por entrada -->
{% for post in all_posts %}
<div class="post-preview">
<a href="{{ url_for('show_post', index=post.id) }}">
<h2 class="post-title">{{ post.title }}</h2>
<h3 class="post-subtitle">{{ post.subtitle }}</h3>
</a>
<p class="post-meta">Posted by <a href="#!">{{ post.author }}</a> on {{ post.date }}</p>
</div>
<hr class="my-4" />
{% endfor %}En post.html, el <header> usa la imagen de cada entrada: style="background-image: url('{{ post.image_url }}')". Aquí no va url_for, porque la imagen viene de Internet.
🛠️ Nota técnica: en el código anterior,
show_postdejarequested_post = Nonesi elidno existe, ypost.htmlfallaría o saldría vacío. Como en el Día 57, devuelve un 404 amable (Reto 2). Y recuerda elint:de la ruta: sin él,blog_post["id"] == indexcompara número con texto y nunca coincide.
⚠️ Errores frecuentes
| Error | Causa | Solución |
|---|---|---|
| Autor y fecha vacíos | La API no tiene esos campos | Agrégalos a tu bin |
post.html sin imagen |
Usaste url_for('static', ...) con una URL de Internet |
url('{{ post.image_url }}') directamente |
| Todas las entradas llevan a la misma página | El enlace no pasa el id |
url_for('show_post', index=post.id) |
| Cambias el bin y el sitio no se actualiza | Los datos se descargan una sola vez, al arrancar | Reinicia el servidor o descarga en cada visita (compara ventajas) |
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1: agrega a tu bin una cuarta entrada escrita por ti y comprueba que aparece sin tocar el código.
- 🟡 Reto 2:
/post/99debe responder con un 404 y un mensaje amable dentro del diseño del sitio. - 🟠 Reto 3: muestra en la portada cuántos minutos se tarda en leer cada entrada (palabras del
body÷ 200, calculado en Python). - 🔴 Reto 4: si la API no responde al arrancar, el servidor no debería caerse. Diseña y prueba un plan B (por ejemplo, leer un
posts_respaldo.jsonlocal) y muestra un aviso discreto en la portada.
6. 🚀 Proyecto del día: el blog Clean Blog completo
(Integra los pasos 1 a 7 del curso: 418 a 424.)
Problema
El blog del Día 57 funciona, pero no se ve profesional ni se adapta al celular, y cada página repite el mismo encabezado.
Objetivo
Un blog en Flask con la plantilla Clean Blog: portada, "Acerca de", "Contacto" y una página por entrada, con encabezado y pie compartidos y datos desde tu API.
Requisitos funcionales
- Cuatro rutas:
/,/about,/contact,/post/<int:index>. - Todas las rutas estáticas con
url_for; ningún 404 en la Consola. header.htmlyfooter.htmlincluidos en todas las páginas.- Portada y páginas de entrada generadas desde tu bin de n:point, con autor y fecha.
- Barra de navegación que se oculta y reaparece (JavaScript funcionando) y menú hamburguesa en celular.
Restricciones técnicas
- Nada de HTML repetido entre páginas, salvo el
<header>con la imagen propia de cada una. - Una sola plantilla
post.htmlpara todas las entradas.
Planificación
- Comprender la plantilla (abre
dist/index.htmlcon doble clic y recórrela). 2. Entradas: plantilla, API. 3. Procesos: mover → corregir rutas → partir en piezas → conectar datos. 4. Salidas: 4 tipos de página. 5. Dividir en los 7 pasos del curso. 6. Diseñar dónde cortarheader.htmlyfooter.html(dibújalo). 7. Implementar. 8. Probar. 9. Corregir. 10. Mejorar.
Construcción paso a paso
- Versión 1: portada sin estilos (paso 2).
- Versión 2: portada con estilos, JavaScript e imagen (paso 3).
- Versión 3: encabezado y pie separados (paso 4).
- Versión 4: "About" y "Contact" (paso 5).
- Versión 5: entradas desde la API (paso 6).
- Versión final: páginas individuales y 404 (paso 7).
Pruebas
| Caso | Resultado esperado |
|---|---|
| Consola en las 4 páginas | Ningún 404 |
| Bajar y subir en la portada | La barra se oculta y reaparece |
| 375 px de ancho | Menú hamburguesa funcional |
| Clic en cada entrada | Su título, subtítulo, imagen, autor, fecha y cuerpo |
/post/99 |
404 amable |
Cambiar el texto de un enlace en header.html |
Cambia en todas las páginas |
🧪 Prueba y depuración
<!-- post.html -->
{% include header.html %}
<header class="masthead" style="background-image: url('{{ url_for('static', filename=post.image_url) }}')">
<h1>{{ post['titulo'] }}</h1>
</header>
<p>{{ post.body }}</p>
{% include "footer.html" %}Encuentra tres problemas: falta poner header.html entre comillas, la imagen viene de Internet y no debe pasar por url_for('static', ...), y la clave es title, no titulo (sale vacío sin error).
⭐ Extensión del proyecto
- Paginación: mostrar 2 entradas por página en la portada con un botón "Entradas anteriores".
- Mostrar la fecha en español (por ejemplo, "27 de septiembre de 2026") formateándola en Python.
7. 🌟 Proyecto extra: el periódico escolar digital
(Proyecto creado para esta adaptación; no existe en el curso original.)
Situación
El comité de comunicaciones del colegio quiere publicar su periódico en la web, con secciones (Deportes, Cultura, Ciencia, Opinión) y un diseño profesional.
Requisitos
- Elige otra plantilla Bootstrap gratuita (de las que menciona el curso o de startbootstrap.com) y justifica la elección (licencia, diseño en celular, secciones).
basecomún con{% include %}: navegación con las secciones y pie con el año actual.- Artículos en un
articulos.jsonlocal conid,seccion,titulo,resumen,cuerpo,autor,fechaeimagen. - Rutas:
/,/seccion/<nombre>(solo los artículos de esa sección, filtrados en Python) y/articulo/<int:id>. - Una sección inexistente o un artículo inexistente muestran un 404 con el diseño del sitio.
Decisiones que debes tomar (y justificar)
- ¿Qué pasa con
/seccion/deportesy/seccion/Deportes? - ¿Cómo evitas publicar el nombre completo de un estudiante autor sin permiso?
Casos de prueba
/seccion/cienciasolo muestra artículos de Ciencia.- Un artículo nuevo agregado al JSON aparece en la portada y en su sección al reiniciar.
8. 🏗️ Proyecto integrador del bloque avanzado — Etapa 1
(Proyecto creado para esta adaptación. Se construye por etapas entre los Días 59 y 70; la descripción completa está en el tutorial del Día 70.)
La Plataforma del Club. El Portal del Grupo (Días 54 a 58) mostraba información. En este bloque se convertirá en una aplicación completa: formularios, base de datos, API propia, usuarios con contraseña y publicación en GitHub.
Etapa 1 (hoy):
- Elige una plantilla Bootstrap gratuita para la Plataforma y ubícala en
templatesystatic. - Separa
header.htmlyfooter.htmlcon{% include %}. - Rutas:
/,/acerca,/contactoy/noticia/<int:id>, con noticias del club en un JSON (tu bin de n:point o un archivo local). - Todas las rutas estáticas con
url_for.
9. 🧩 Comprueba lo aprendido
Conceptuales
- ¿Qué hace
{% include "header.html" %}? ¿En qué se parece a llamar una función? - ¿Por qué el
<header>con la imagen de fondo no se mueve aheader.html? - ¿Por qué
header.htmlno es un HTML válido por sí solo? - ¿Cuándo usas
url_for('static', ...)para una imagen y cuándo no? - ¿Qué pasa en Jinja si pides
{{ post.author }}y la entrada no tiene esa clave?
Análisis de código
<header class="masthead" style="background-image: url('{{ url_for('static', filename='assets/img/about-bg.jpg') }}')">¿Qué texto exacto le llega al navegador?
Predicción
Si header.html tiene un error de sintaxis de Jinja, ¿qué páginas fallan?
Depuración
La barra de navegación del blog no se oculta al bajar, pero los estilos sí cargan. Menciona dos causas posibles y cómo comprobarías cada una en las herramientas de desarrollo.
Modificación
Cambia la ruta de las entradas de /post/<int:index> a /entrada/<int:index>. ¿Cuántos archivos tienes que editar si usaste url_for en todos los enlaces?
Creación
Crea alerta.html con un aviso de Bootstrap ("¡Nuevas entradas cada lunes!") e inclúyelo solo en la portada, justo debajo del encabezado.
10. 🤖 Programar con apoyo de IA
Ciclo de trabajo: formula el problema → inténtalo tú → consulta a la IA → lee y entiende → prueba → compara con la documentación oficial → modifica → explica con tus palabras.
Actividad 1 — Corregir rutas en lote. Pide a una IA que convierta todas las rutas de index.html a url_for. Revisa si también cambió (mal) las URL de Internet, o si olvidó la del atributo style.
Actividad 2 — include o extends. Pregunta a una IA la diferencia. Compara con la documentación de Jinja (Include y Template Inheritance) y escribe una tabla propia.
Actividad 3 — El formulario que no envía. Muestra a una IA el <form> de Clean Blog con data-sb-form-api-token. ¿Identifica que depende de un servicio externo? Contrasta con la nota técnica de la sección 4.
11. 🛠️ Correcciones técnicas al material original
| Archivo | Afirmación o comportamiento del curso | Observación | Tratamiento |
|---|---|---|---|
423 |
Reemplazar title, subtitle, author y dates con los datos de la API |
La API de ejemplo no tiene author ni date (verificado); hay que agregarlos al bin propio |
Sección 5 |
418, 420 |
Estructura de carpetas de la imagen del curso | Clean Blog 6.0.9 usa assets/img/, css/ y js/ (verificado); versiones antiguas difieren |
Nota técnica de la sección 1 |
422 |
Hacer funcionar la página de contacto | El formulario de la plantilla depende de SB Forms (verificado); no envía a Flask | Nota técnica de la sección 4 (se resuelve el Día 60) |
424 |
Reutilizar el código del Día 55 para las entradas | El blog con entradas individuales es del Día 57; además hay que manejar un id inexistente |
Sección 5 y Reto 2 |
420, 421 |
Documentación de Flask 2.3 y Jinja 3.0 | Siguen siendo válidas en Flask 3.1 y Jinja 3.1 (verificado) | — |
12. 📋 Matriz de trazabilidad
| Contenido original | Archivo de origen | Concepto desarrollado | Ejercicio original | Reto adicional | Proyecto relacionado |
|---|---|---|---|---|---|
| Mejorar el blog con Bootstrap, plantillas gratuitas (bootstrapmade, ejemplos de Bootstrap, creative-tim), navegación, entradas dinámicas, diseño responsivo | 417 |
Sección 0 | — | Principio DRY | Sección 6 |
Descargar Clean Blog, renombrar, static y templates, header.html, footer.html, main.py |
418 |
Sección 1 | Paso 1 | Estructura verificada | Secciones 6 y 8 |
Portada en localhost:5000 |
419 |
Sección 1 | Paso 2 | — | Sección 6 |
Rutas estáticas con URL dinámica, JavaScript del pie, imagen home-bg.jpg |
420 |
Sección 2 | Paso 3 | Tabla de rutas por corregir | Secciones 6 y 8 |
{% include %}, mover <head>, navegación y <footer> |
421 |
Sección 3 | Paso 4 | include contra herencia |
Secciones 6, 7 y 8 |
Quitar "Sample Post", url_for en la navegación, rutas /about y /contact, imágenes de fondo |
422 |
Sección 4 | Paso 5 | Retos 1–4 de la sección 4 | Secciones 6 y 8 |
Bin propio en n:point, datos de la API, título, subtítulo, autor y fecha, notación de punto, ciclo for, imagen de Unsplash |
423 |
Sección 5 | Paso 6 | Retos 1–4 de la sección 5 | Secciones 6 y 8 |
post.html con título, subtítulo, imagen, fecha, autor y cuerpo; "programar es experimentar" |
424 |
Sección 5 | Paso 7 | 404 amable | Sección 6 |
| (No existe en el curso original) | — | — | — | Periódico escolar digital | Sección 7 |
| (No existe en el curso original) | — | — | — | Plataforma del Club, etapa 1 | Sección 8 |
13. 📦 Evidencias de aprendizaje del Día 59
- El blog completo, con capturas de las cuatro páginas en escritorio y en celular.
- Tu bin de n:point con
authorydate(captura o enlace). - El dibujo de dónde cortaste
header.htmlyfooter.html. - El periódico escolar digital (proyecto extra).
- La etapa 1 de la Plataforma del Club.
- Cuaderno de errores: HTML en
templates/dist, rutas relativas,url_fordentro delstyle,includesin comillas, navegación duplicada, JavaScript sin corregir, claves inexistentes en la API,url_for('static')con una URL de Internet,idinexistente.