Saltar al contenido
Volver al inicio 100 Días de Código en Python — Día 59: El blog con Bootstrap (proyecto del blog, parte 2)

100 Días de Código en Python — Día 59: El blog con Bootstrap (proyecto del blog, parte 2)

Nivel: Grado décimo Área: Tecnología e Informática Modalidad: Tutorial práctico Fuente: Curso "100 Days of Code: The Complete Python Pro Bootcamp" (Angela Yu, App Brewery). Contenido del Día 59 (nivel avanzado).

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 include y 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 con id, title, subtitle, body e image_url (las tres imágenes responden), sin author ni date. Se ejecutaron con el cliente de pruebas de Flask los ejemplos de include.


Tabla de contenidos

  1. Antes de empezar: mejorar el blog sin escribir Bootstrap
  2. Pasos 1 y 2: el proyecto de inicio y la portada
  3. Paso 3: arreglar el encabezado y el pie de página
  4. Paso 4: {% include %} para no repetir código
  5. Paso 5: las páginas "Acerca de" y "Contacto"
  6. Pasos 6 y 7: entradas desde una API y páginas individuales
  7. 🚀 Proyecto del día: el blog Clean Blog completo
  8. 🌟 Proyecto extra: el periódico escolar digital
  9. 🏗️ Proyecto integrador del bloque avanzado — Etapa 1
  10. 🧩 Comprueba lo aprendido
  11. 🤖 Programar con apoyo de IA
  12. 🛠️ Correcciones técnicas al material original
  13. 📋 Matriz de trazabilidad
  14. 📦 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.

  1. Descarga la plantilla Clean Blog de Start Bootstrap: https://startbootstrap.com/previews/clean-blog/.
  2. Descomprime el archivo y renombra la carpeta a "upgraded blog".
  3. Ábrela en PyCharm y:
    • A. Crea las carpetas static y templates.
    • B. Mueve los archivos HTML a templates y todas las carpetas a static.
    • C. Crea header.html, footer.html y el importantísimo main.py.
    • D. Comprueba que la estructura coincide con la imagen del curso.

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:

Texto
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.js
Python
from 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 con img/ 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.

  1. Arregla el <head> de index.html para 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 carpeta static" (documentación de Flask sobre archivos estáticos).
  2. 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.
  3. Haz que reaparezca la imagen de fondo home-bg.jpg corrigiendo 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:

HTML
{% 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).

  1. Quita de index.html el <head> y la navegación, y ponlos en header.html.
  2. Quita el <footer> de index.html y ponlo en footer.html.
  3. Usa include para que el sitio funcione exactamente igual que antes.

📖 ¿Dónde se corta? (adaptación)

Texto
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

  1. En header.html, borra el enlace de la navegación que apunta a "Sample Post".
  2. Cambia los href con url_for para que la navegación lleve a "About" y "Contact".
  3. Actualiza main.py para que esas rutas existan.
  4. Actualiza about.html y contact.html para que usen el encabezado y el pie.
  5. Haz que aparezcan las imágenes de fondo de "About" y "Contact". Pista: mira el atributo style del <header>; las imágenes son archivos estáticos.

📖 Explicación conceptual

Python
@app.route("/about")
def about():
    return render_template("about.html")


@app.route("/contact")
def contact():
    return render_template("contact.html")
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.html de la plantilla usa SB Forms, un servicio de Start Bootstrap (data-sb-form-api-token="API_TOKEN" y el script sb-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 un contact.html simplificado 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.endpoint está 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_template y 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:

  1. 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 como https://api.npoint.io/674f5423f73deab1e9a7.
  2. En main.py, obtén los datos de la API.
  3. En la portada, reemplaza el title, el subtitle, el author y la date de 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 for en 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).

Python
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)
HTML
<!-- 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_post deja requested_post = None si el id no existe, y post.html fallaría o saldría vacío. Como en el Día 57, devuelve un 404 amable (Reto 2). Y recuerda el int: de la ruta: sin él, blog_post["id"] == index compara 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/99 debe 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.json local) 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

  1. Cuatro rutas: /, /about, /contact, /post/<int:index>.
  2. Todas las rutas estáticas con url_for; ningún 404 en la Consola.
  3. header.html y footer.html incluidos en todas las páginas.
  4. Portada y páginas de entrada generadas desde tu bin de n:point, con autor y fecha.
  5. 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.html para todas las entradas.

Planificación

  1. Comprender la plantilla (abre dist/index.html con 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 cortar header.html y footer.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

HTML
<!-- 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

  1. 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).
  2. base común con {% include %}: navegación con las secciones y pie con el año actual.
  3. Artículos en un articulos.json local con id, seccion, titulo, resumen, cuerpo, autor, fecha e imagen.
  4. Rutas: /, /seccion/<nombre> (solo los artículos de esa sección, filtrados en Python) y /articulo/<int:id>.
  5. 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/deportes y /seccion/Deportes?
  • ¿Cómo evitas publicar el nombre completo de un estudiante autor sin permiso?

Casos de prueba

  • /seccion/ciencia solo 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):

  1. Elige una plantilla Bootstrap gratuita para la Plataforma y ubícala en templates y static.
  2. Separa header.html y footer.html con {% include %}.
  3. Rutas: /, /acerca, /contacto y /noticia/<int:id>, con noticias del club en un JSON (tu bin de n:point o un archivo local).
  4. Todas las rutas estáticas con url_for.

9. 🧩 Comprueba lo aprendido

Conceptuales

  1. ¿Qué hace {% include "header.html" %}? ¿En qué se parece a llamar una función?
  2. ¿Por qué el <header> con la imagen de fondo no se mueve a header.html?
  3. ¿Por qué header.html no es un HTML válido por sí solo?
  4. ¿Cuándo usas url_for('static', ...) para una imagen y cuándo no?
  5. ¿Qué pasa en Jinja si pides {{ post.author }} y la entrada no tiene esa clave?

Análisis de código

HTML
<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 author y date (captura o enlace).
  • El dibujo de dónde cortaste header.html y footer.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_for dentro del style, include sin comillas, navegación duplicada, JavaScript sin corregir, claves inexistentes en la API, url_for('static') con una URL de Internet, id inexistente.