Skip to content

Commit 9dd2f31

Browse files
alephclaude
andcommitted
fix: resolve 404 errors on subdirectory URLs with CloudFront Functions
Add CloudFront Functions to rewrite directory URLs to include index.html. This fixes 404 errors on pages like /meetups/, /comunidad/ponentes/, etc. Changes: - Add CloudFront Function for production and staging environments - Associate functions with default cache behavior on distributions - Add comprehensive documentation in FIX_404_ERRORS.md The solution uses CloudFront Functions to intercept viewer requests and automatically append index.html to URLs ending with / or without file extensions, enabling MkDocs directory URLs to work correctly with S3/CloudFront. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e46feaa commit 9dd2f31

File tree

5 files changed

+298
-16
lines changed

5 files changed

+298
-16
lines changed

FIX_404_ERRORS.md

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
# Solución a Errores 404 en pythoncdmx.org
2+
3+
## 📋 Resumen del Problema
4+
5+
**Problema identificado:** Las URLs de subdirectorios (`/meetups/`, `/comunidad/`, etc.) devuelven error 404, mientras que la página principal funciona correctamente.
6+
7+
**Causa raíz:**
8+
- MkDocs genera el sitio con `--use-directory-urls`, creando URLs limpias como `/meetups/``site/meetups/index.html`
9+
- CloudFront tiene configurado `default_root_object = "index.html"` que **solo funciona para la raíz** (`/`)
10+
- Para subdirectorios, CloudFront busca un objeto llamado `meetups/` en S3 (sin `index.html`)
11+
- Como ese objeto no existe, S3 devuelve 403, que CloudFront convierte en 404
12+
13+
## ✅ Solución Implementada
14+
15+
Se implementó **CloudFront Functions** para reescribir automáticamente las URLs y agregar `index.html` a las rutas que terminan en `/` o no tienen extensión.
16+
17+
### Archivos Modificados/Creados
18+
19+
#### 1. **Nuevo:** `terraform/cloudfront-function.tf`
20+
- Crea dos CloudFront Functions (producción y staging)
21+
- Función JavaScript que intercepta requests y añade `index.html` automáticamente
22+
- Maneja dos casos:
23+
- URLs que terminan en `/` → Añade `index.html`
24+
- URLs sin extensión de archivo → Añade `/index.html`
25+
26+
#### 2. **Modificado:** `terraform/cloudfront.tf`
27+
- Líneas 46-50: Asocia la función CloudFront al `default_cache_behavior`
28+
- La función se ejecuta en el evento `viewer-request` (antes de llegar a S3)
29+
30+
#### 3. **Modificado:** `terraform/cloudfront-staging.tf`
31+
- Líneas 46-50: Asocia la función CloudFront de staging al `default_cache_behavior`
32+
- Misma lógica aplicada al ambiente de staging
33+
34+
## 🚀 Pasos para Desplegar
35+
36+
### Prerequisitos
37+
- Acceso a AWS con credenciales configuradas
38+
- Terraform instalado
39+
- Variables de Terraform configuradas (archivo `terraform.tfvars`)
40+
41+
### Despliegue
42+
43+
1. **Navega al directorio de Terraform:**
44+
```bash
45+
cd terraform
46+
```
47+
48+
2. **Revisa el plan de Terraform:**
49+
```bash
50+
terraform plan
51+
```
52+
53+
Deberías ver:
54+
- `+ aws_cloudfront_function.directory_index` (nuevo)
55+
- `+ aws_cloudfront_function.directory_index_staging` (nuevo)
56+
- `~ aws_cloudfront_distribution.website` (modificado)
57+
- `~ aws_cloudfront_distribution.website_staging` (modificado)
58+
59+
3. **Aplica los cambios:**
60+
```bash
61+
terraform apply
62+
```
63+
64+
4. **Confirma los cambios:** Escribe `yes` cuando se te solicite
65+
66+
### Tiempo de Propagación
67+
68+
- **CloudFront Functions:** Se despliegan inmediatamente en todas las edge locations
69+
- **Distribución de CloudFront:** Puede tardar 5-15 minutos en propagarse completamente
70+
- **Cache:** Si hay contenido en caché, puede tardar hasta 1 hora (basado en `max_ttl`)
71+
72+
### Invalidación de Caché (Opcional pero Recomendado)
73+
74+
Para aplicar los cambios inmediatamente sin esperar la expiración del caché:
75+
76+
```bash
77+
# Para producción
78+
aws cloudfront create-invalidation \
79+
--distribution-id <DISTRIBUTION_ID> \
80+
--paths "/*"
81+
82+
# Para staging
83+
aws cloudfront create-invalidation \
84+
--distribution-id <STAGING_DISTRIBUTION_ID> \
85+
--paths "/*"
86+
```
87+
88+
Puedes obtener los Distribution IDs con:
89+
```bash
90+
terraform output cloudfront_distribution_id
91+
terraform output cloudfront_staging_distribution_id
92+
```
93+
94+
## 🧪 Verificación
95+
96+
Una vez desplegado, verifica que las siguientes URLs funcionan:
97+
98+
### Producción (pythoncdmx.org)
99+
-`https://pythoncdmx.org/` (ya funcionaba)
100+
-`https://pythoncdmx.org/meetups/`
101+
-`https://pythoncdmx.org/meetups/2025/`
102+
-`https://pythoncdmx.org/comunidad/`
103+
-`https://pythoncdmx.org/comunidad/ponentes/`
104+
-`https://pythoncdmx.org/comunidad/voluntarios/`
105+
-`https://pythoncdmx.org/blog/`
106+
107+
### Staging (si aplica)
108+
- ✅ Todas las rutas equivalentes en el dominio de staging
109+
110+
## 📊 Impacto y Beneficios
111+
112+
### Ventajas de la Solución
113+
-**URLs limpias:** Mantiene `/meetups/` en lugar de `/meetups.html`
114+
-**SEO amigable:** Las URLs siguen siendo las mismas
115+
-**Sin cambios en el código:** No requiere modificar MkDocs
116+
-**Bajo costo:** CloudFront Functions es prácticamente gratis ($0.10 por millón de invocaciones)
117+
-**Alta performance:** Se ejecuta en edge locations (latencia mínima)
118+
-**Escalable:** Funciona automáticamente para cualquier nueva página
119+
120+
### Costo Estimado
121+
- **CloudFront Functions:** ~$0.10 por millón de requests
122+
- Para un sitio con 100,000 visitas/mes: **~$0.01/mes**
123+
124+
## 🔍 Debugging
125+
126+
Si después del despliegue aún hay errores 404:
127+
128+
1. **Verifica que la función esté asociada:**
129+
```bash
130+
aws cloudfront get-distribution --id <DISTRIBUTION_ID> \
131+
| jq '.Distribution.DistributionConfig.DefaultCacheBehavior.FunctionAssociations'
132+
```
133+
134+
2. **Verifica que la función esté publicada:**
135+
```bash
136+
aws cloudfront list-functions
137+
```
138+
139+
3. **Revisa CloudWatch Logs (si está habilitado):**
140+
```bash
141+
aws logs tail /aws/cloudfront/function/pythoncdmx-directory-index --follow
142+
```
143+
144+
4. **Invalida el caché de CloudFront** (ver comando arriba)
145+
146+
5. **Prueba con curl para ver headers:**
147+
```bash
148+
curl -I https://pythoncdmx.org/meetups/
149+
```
150+
151+
## 📝 Notas Técnicas
152+
153+
### Cómo Funciona la CloudFront Function
154+
155+
```javascript
156+
function handler(event) {
157+
var request = event.request;
158+
var uri = request.uri;
159+
160+
// Ejemplo: /meetups/ → /meetups/index.html
161+
if (uri.endsWith('/')) {
162+
request.uri += 'index.html';
163+
}
164+
// Ejemplo: /meetups → /meetups/index.html
165+
else if (!uri.includes('.')) {
166+
request.uri += '/index.html';
167+
}
168+
169+
return request;
170+
}
171+
```
172+
173+
**Flujo de ejecución:**
174+
1. Usuario solicita `https://pythoncdmx.org/meetups/`
175+
2. CloudFront recibe el request en la edge location
176+
3. **CloudFront Function** intercepta y reescribe: `/meetups/``/meetups/index.html`
177+
4. CloudFront solicita a S3: `s3://bucket/meetups/index.html`
178+
5. S3 devuelve el archivo (existe en S3 gracias a MkDocs)
179+
6. CloudFront devuelve la respuesta al usuario
180+
181+
### Alternativas Consideradas (No Implementadas)
182+
183+
1. **Lambda@Edge:** Más potente pero:
184+
- ❌ Más costoso (~$0.60 por millón vs $0.10)
185+
- ❌ Mayor latencia (ejecuta en regional edge cache)
186+
- ❌ Más complejo de mantener
187+
188+
2. **Cambiar a `--no-directory-urls`:**
189+
- ❌ URLs menos amigables (`/meetups.html`)
190+
- ❌ Rompe links existentes
191+
- ❌ Peor SEO
192+
193+
3. **S3 Redirects:**
194+
- ❌ No funciona con CloudFront OAC
195+
- ❌ Requiere S3 public (inseguro)
196+
197+
## 🎯 Próximos Pasos
198+
199+
1. **Desplegar los cambios** siguiendo la sección "Pasos para Desplegar"
200+
2. **Verificar** que todas las URLs funcionan correctamente
201+
3. **Monitorear** CloudFront metrics durante las primeras 24 horas
202+
4. **Documentar** en el README del proyecto que se usa CloudFront Functions
203+
204+
## 🆘 Soporte
205+
206+
Si encuentras problemas durante el despliegue:
207+
208+
1. Revisa el output de `terraform plan` y `terraform apply`
209+
2. Verifica los logs de CloudWatch (si están habilitados)
210+
3. Consulta la documentación de AWS:
211+
- [CloudFront Functions](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-functions.html)
212+
- [CloudFront Distribution](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-working-with.html)
213+
214+
---
215+
216+
**Fecha de implementación:** 2025-10-25
217+
**Autor:** Claude Code
218+
**Versión:** 1.0

terraform/cloudfront-function.tf

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# CloudFront Function to handle directory index rewriting for production
2+
# This function adds index.html to requests ending with /
3+
resource "aws_cloudfront_function" "directory_index" {
4+
name = "pythoncdmx-directory-index"
5+
runtime = "cloudfront-js-1.0"
6+
comment = "Rewrites URLs ending with / to /index.html for MkDocs directory structure"
7+
publish = true
8+
9+
code = <<-EOT
10+
function handler(event) {
11+
var request = event.request;
12+
var uri = request.uri;
13+
14+
// Check if the URI ends with '/'
15+
if (uri.endsWith('/')) {
16+
request.uri += 'index.html';
17+
}
18+
// Check if the URI doesn't have a file extension
19+
else if (!uri.includes('.')) {
20+
request.uri += '/index.html';
21+
}
22+
23+
return request;
24+
}
25+
EOT
26+
}
27+
28+
# CloudFront Function for staging environment
29+
resource "aws_cloudfront_function" "directory_index_staging" {
30+
name = "pythoncdmx-directory-index-staging"
31+
runtime = "cloudfront-js-1.0"
32+
comment = "Rewrites URLs ending with / to /index.html for MkDocs directory structure (Staging)"
33+
publish = true
34+
35+
code = <<-EOT
36+
function handler(event) {
37+
var request = event.request;
38+
var uri = request.uri;
39+
40+
// Check if the URI ends with '/'
41+
if (uri.endsWith('/')) {
42+
request.uri += 'index.html';
43+
}
44+
// Check if the URI doesn't have a file extension
45+
else if (!uri.includes('.')) {
46+
request.uri += '/index.html';
47+
}
48+
49+
return request;
50+
}
51+
EOT
52+
}

terraform/cloudfront-staging.tf

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,15 @@ resource "aws_cloudfront_distribution" "website_staging" {
3939

4040
viewer_protocol_policy = "redirect-to-https"
4141
min_ttl = 0
42-
default_ttl = 300 # 5 minutes - shorter cache for staging
43-
max_ttl = 3600 # 1 hour - shorter cache for staging
42+
default_ttl = 300 # 5 minutes - shorter cache for staging
43+
max_ttl = 3600 # 1 hour - shorter cache for staging
4444
compress = true
45+
46+
# Associate CloudFront Function for directory index rewriting
47+
function_association {
48+
event_type = "viewer-request"
49+
function_arn = aws_cloudfront_function.directory_index_staging.arn
50+
}
4551
}
4652

4753
# Cache behavior for static assets (CSS) - shorter cache for staging
@@ -60,8 +66,8 @@ resource "aws_cloudfront_distribution" "website_staging" {
6066

6167
viewer_protocol_policy = "redirect-to-https"
6268
min_ttl = 0
63-
default_ttl = 1800 # 30 minutes
64-
max_ttl = 7200 # 2 hours
69+
default_ttl = 1800 # 30 minutes
70+
max_ttl = 7200 # 2 hours
6571
compress = true
6672
}
6773

@@ -81,8 +87,8 @@ resource "aws_cloudfront_distribution" "website_staging" {
8187

8288
viewer_protocol_policy = "redirect-to-https"
8389
min_ttl = 0
84-
default_ttl = 1800 # 30 minutes
85-
max_ttl = 7200 # 2 hours
90+
default_ttl = 1800 # 30 minutes
91+
max_ttl = 7200 # 2 hours
8692
compress = true
8793
}
8894

@@ -102,8 +108,8 @@ resource "aws_cloudfront_distribution" "website_staging" {
102108

103109
viewer_protocol_policy = "redirect-to-https"
104110
min_ttl = 0
105-
default_ttl = 3600 # 1 hour
106-
max_ttl = 86400 # 24 hours
111+
default_ttl = 3600 # 1 hour
112+
max_ttl = 86400 # 24 hours
107113
compress = true
108114
}
109115

terraform/cloudfront.tf

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,15 @@ resource "aws_cloudfront_distribution" "website" {
3939

4040
viewer_protocol_policy = "redirect-to-https"
4141
min_ttl = 0
42-
default_ttl = 3600 # 1 hour
43-
max_ttl = 86400 # 24 hours
42+
default_ttl = 3600 # 1 hour
43+
max_ttl = 86400 # 24 hours
4444
compress = true
45+
46+
# Associate CloudFront Function for directory index rewriting
47+
function_association {
48+
event_type = "viewer-request"
49+
function_arn = aws_cloudfront_function.directory_index.arn
50+
}
4551
}
4652

4753
# Cache behavior for static assets (images, CSS, JS)
@@ -60,7 +66,7 @@ resource "aws_cloudfront_distribution" "website" {
6066

6167
viewer_protocol_policy = "redirect-to-https"
6268
min_ttl = 0
63-
default_ttl = 86400 # 24 hours
69+
default_ttl = 86400 # 24 hours
6470
max_ttl = 31536000 # 1 year
6571
compress = true
6672
}
@@ -80,7 +86,7 @@ resource "aws_cloudfront_distribution" "website" {
8086

8187
viewer_protocol_policy = "redirect-to-https"
8288
min_ttl = 0
83-
default_ttl = 86400 # 24 hours
89+
default_ttl = 86400 # 24 hours
8490
max_ttl = 31536000 # 1 year
8591
compress = true
8692
}
@@ -100,7 +106,7 @@ resource "aws_cloudfront_distribution" "website" {
100106

101107
viewer_protocol_policy = "redirect-to-https"
102108
min_ttl = 0
103-
default_ttl = 604800 # 7 days
109+
default_ttl = 604800 # 7 days
104110
max_ttl = 31536000 # 1 year
105111
compress = true
106112
}

terraform/outputs.tf

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ output "certificate_validation_records" {
2727
description = "DNS validation records for the certificate"
2828
value = [
2929
for dvo in aws_acm_certificate.website.domain_validation_options : {
30-
name = dvo.resource_record_name
31-
type = dvo.resource_record_type
32-
value = dvo.resource_record_value
30+
name = dvo.resource_record_name
31+
type = dvo.resource_record_type
32+
value = dvo.resource_record_value
3333
}
3434
]
3535
}

0 commit comments

Comments
 (0)