diff --git a/.commitlintrc.json b/.commitlintrc.json
new file mode 100644
index 0000000..7a4907e
--- /dev/null
+++ b/.commitlintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": ["@commitlint/config-conventional"]
+}
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d2cbd74..a79d760 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -55,11 +55,10 @@ jobs:
echo "✅ All links working!"
pkill -f "mkdocs serve" || true
- # Solo para main: guardar el sitio construido
+ # Upload build artifact for validation/review
- name: Upload build artifact
- if: github.ref == 'refs/heads/main'
uses: actions/upload-artifact@v4
with:
name: site
path: site/
- retention-days: 1
+ retention-days: 3
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
deleted file mode 100644
index f65287e..0000000
--- a/.github/workflows/ci-cd.yml
+++ /dev/null
@@ -1,51 +0,0 @@
-name: CI/CD Pipeline
-
-on:
- push:
- branches: [ main ]
- pull_request:
- branches: [ main ]
-
-permissions:
- contents: read
- pages: write
- id-token: write
-
-jobs:
- build-and-validate:
- name: Build and Validate
- runs-on: ubuntu-latest
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
-
- - name: Set up Python
- uses: actions/setup-python@v4
- with:
- python-version: '3.x'
-
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -r requirements.txt
-
- - name: Build MkDocs site
- run: mkdocs build --strict --use-directory-urls
-
- - name: Upload Pages artifact
- uses: actions/upload-pages-artifact@v3
- with:
- path: site/
-
- deploy:
- name: Deploy to GitHub Pages
- if: github.ref == 'refs/heads/main'
- needs: build-and-validate
- runs-on: ubuntu-latest
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
- steps:
- - name: Deploy to GitHub Pages
- id: deployment
- uses: actions/deploy-pages@v4
diff --git a/.github/workflows/deploy-aws.yml b/.github/workflows/deploy-aws.yml
new file mode 100644
index 0000000..6b2bd5d
--- /dev/null
+++ b/.github/workflows/deploy-aws.yml
@@ -0,0 +1,98 @@
+name: Deploy to AWS S3 + CloudFront
+
+on:
+ push:
+ branches: [ main ]
+
+permissions:
+ contents: read
+ id-token: write # Required for AWS OIDC authentication
+
+
+jobs:
+ commit_lint:
+ name: Validate Commit Messages
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Validate PR Title
+ uses: wagoid/commitlint-github-action@v5
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ configFile: .commitlintrc.json
+
+ build-and-deploy:
+ needs: commit_lint
+ name: Build and Deploy to AWS
+ runs-on: ubuntu-latest
+ environment:
+ name: aws-prod
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+ cache: 'pip'
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+
+ - name: Build MkDocs site
+ run: mkdocs build --strict --use-directory-urls
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Configure AWS credentials
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
+ aws-region: ${{ secrets.AWS_REGION }}
+
+ - name: Sync to S3
+ run: |
+ aws s3 sync site/ s3://${{ secrets.AWS_S3_BUCKET }}/ \
+ --delete \
+ --cache-control "public, max-age=3600" \
+ --exclude "*.html" \
+ --exclude "sitemap.xml"
+
+ # Upload HTML files with shorter cache
+ aws s3 sync site/ s3://${{ secrets.AWS_S3_BUCKET }}/ \
+ --cache-control "public, max-age=600, must-revalidate" \
+ --content-type "text/html; charset=utf-8" \
+ --exclude "*" \
+ --include "*.html"
+
+ # Upload sitemap with no cache
+ aws s3 sync site/ s3://${{ secrets.AWS_S3_BUCKET }}/ \
+ --cache-control "public, max-age=0, must-revalidate" \
+ --exclude "*" \
+ --include "sitemap.xml"
+
+ cleanup-staging:
+ name: Stop Staging Site
+ needs: build-and-deploy
+ runs-on: ubuntu-latest
+ environment:
+ name: aws-stag
+ steps:
+ - name: Configure AWS credentials
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
+ aws-region: ${{ secrets.AWS_REGION }}
+
+ - name: Stop Staging Site
+ run: |
+ aws s3 rm s3://${{ secrets.AWS_S3_BUCKET }}/staging/ --recursive
\ No newline at end of file
diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml
new file mode 100644
index 0000000..866919a
--- /dev/null
+++ b/.github/workflows/deploy-staging.yml
@@ -0,0 +1,91 @@
+name: Deploy to Staging (AWS S3 + CloudFront)
+
+on:
+ push:
+ branches: [ staging ]
+
+permissions:
+ contents: read
+ id-token: write # Required for AWS OIDC authentication
+
+
+jobs:
+
+ commit_lint:
+ name: Validate Commit Messages
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Validate PR Title
+ uses: wagoid/commitlint-github-action@v5
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ configFile: .commitlintrc.json
+ build-and-deploy-staging:
+ name: Build and Deploy to Staging
+ needs: commit_lint
+ runs-on: ubuntu-latest
+ environment:
+ name: aws-stag
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+ cache: 'pip'
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+
+ - name: Build MkDocs site
+ run: mkdocs build --strict --use-directory-urls
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Add staging banner to site
+ run: |
+ # Add a staging environment banner to all HTML files
+ find site -name "*.html" -type f -exec sed -i.bak '/
\
+ 🚧 STAGING ENVIRONMENT - NOT FOR PRODUCTION USE 🚧\
+ ' {} \;
+ # Clean up backup files
+ find site -name "*.bak" -type f -delete
+
+ - name: Configure AWS credentials
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
+ aws-region: ${{ secrets.AWS_REGION }}
+
+ - name: Sync to S3 (Staging)
+ run: |
+ aws s3 sync site/ s3://${{ secrets.AWS_S3_BUCKET }}/ \
+ --delete \
+ --cache-control "public, max-age=300" \
+ --exclude "*.html" \
+ --exclude "sitemap.xml"
+
+ # Upload HTML files with shorter cache for staging
+ aws s3 sync site/ s3://${{ secrets.AWS_S3_BUCKET }}/ \
+ --cache-control "public, max-age=60, must-revalidate" \
+ --content-type "text/html; charset=utf-8" \
+ --exclude "*" \
+ --include "*.html"
+
+ # Upload sitemap with no cache
+ aws s3 sync site/ s3://${{ secrets.AWS_S3_BUCKET }}/ \
+ --cache-control "public, max-age=0, must-revalidate" \
+ --exclude "*" \
+ --include "sitemap.xml"
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
deleted file mode 100644
index 0baa5b3..0000000
--- a/.github/workflows/deploy.yml
+++ /dev/null
@@ -1,46 +0,0 @@
-name: Deploy to GitHub Pages
-
-on:
- workflow_run:
- workflows: ["Build and Validate"]
- types:
- - completed
- branches: [ main ]
-
-permissions:
- contents: read
- pages: write
- id-token: write
-
-concurrency:
- group: "pages"
- cancel-in-progress: false
-
-jobs:
- deploy:
- name: Deploy to GitHub Pages
- if: github.event.workflow_run.conclusion == 'success'
- runs-on: ubuntu-latest
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
- steps:
- - name: Download build artifact
- uses: actions/download-artifact@v4
- with:
- name: site
- path: site/
- github-token: ${{ secrets.GITHUB_TOKEN }}
- run-id: ${{ github.event.workflow_run.id }}
-
- - name: Setup Pages
- uses: actions/configure-pages@v4
-
- - name: Upload to GitHub Pages
- uses: actions/upload-pages-artifact@v3
- with:
- path: site/
-
- - name: Deploy to GitHub Pages
- id: deployment
- uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
index 5403a8a..9305398 100644
--- a/.gitignore
+++ b/.gitignore
@@ -271,3 +271,6 @@ Thumbs.db
# Temporary files
*.tmp
*.temp
+
+# Generated files
+broken_links.json
diff --git a/broken_links.json b/broken_links.json
index bf87eb7..64bd6bb 100644
--- a/broken_links.json
+++ b/broken_links.json
@@ -1,35 +1,36 @@
{
"summary": {
- "total_files_scanned": 35,
- "working_links": 79,
- "broken_links": 0,
+ "total_files_scanned": 45,
+ "working_links": 80,
+ "broken_links": 2,
"base_url": "http://127.0.0.1:8000"
},
- "broken_links": [],
- "working_links": [
+ "broken_links": [
{
- "file": "about.md",
- "text": "guía para ponentes",
- "url": "comunidad/ponentes/",
- "full_url": "http://127.0.0.1:8000/comunidad/ponentes/",
- "status": "200 OK",
- "line": 42,
+ "file": "blog/media/README.md",
+ "text": "alt text",
+ "url": "media/nombre-del-articulo/imagen.png",
+ "full_url": "http://127.0.0.1:8000/blog/media/media/nombre-del-articulo/imagen.png",
+ "status": "404 Not Found",
+ "line": 55,
"link_type": "markdown"
},
{
- "file": "about.md",
- "text": "nuestra página de voluntarios",
- "url": "/comunidad/voluntarios/",
- "full_url": "http://127.0.0.1:8000/comunidad/voluntarios/",
- "status": "200 OK",
- "line": 46,
+ "file": "comunidad/como-contribuir.md",
+ "text": "formas de contribuir",
+ "url": "#formas-de-contribuir",
+ "full_url": "http://127.0.0.1:8000/comunidad/como-contribuir.html#formas-de-contribuir",
+ "status": "404 Not Found",
+ "line": 63,
"link_type": "markdown"
- },
+ }
+ ],
+ "working_links": [
{
"file": "index.md",
"text": "Explorar charlas",
- "url": "/meetups/",
- "full_url": "http://127.0.0.1:8000/meetups/",
+ "url": "/meetups/index.md",
+ "full_url": "http://127.0.0.1:8000/meetups/index.html",
"status": "200 OK",
"line": 8,
"link_type": "html"
@@ -82,370 +83,352 @@
{
"file": "index.md",
"text": "Ver Eventos",
- "url": "/meetups/",
- "full_url": "http://127.0.0.1:8000/meetups/",
+ "url": "/meetups/index.md",
+ "full_url": "http://127.0.0.1:8000/meetups/index.html",
"status": "200 OK",
"line": 49,
"link_type": "html"
},
{
- "file": "meetups/2023/index.md",
- "text": "Ver detalles",
- "url": "202311-noviembre",
- "full_url": "http://127.0.0.1:8000/202311-noviembre",
- "status": "200 OK",
- "line": 15,
- "link_type": "markdown"
- },
- {
- "file": "meetups/2023/index.md",
- "text": "Ver detalles",
- "url": "202311-noviembre",
- "full_url": "http://127.0.0.1:8000/202311-noviembre",
- "status": "200 OK",
- "line": 16,
- "link_type": "markdown"
- },
- {
- "file": "meetups/2023/index.md",
- "text": "Ver detalles",
- "url": "202310-octubre",
- "full_url": "http://127.0.0.1:8000/202310-octubre",
- "status": "200 OK",
- "line": 17,
- "link_type": "markdown"
- },
- {
- "file": "meetups/2023/index.md",
- "text": "Ver detalles",
- "url": "202309-septiembre",
- "full_url": "http://127.0.0.1:8000/202309-septiembre",
- "status": "200 OK",
- "line": 18,
- "link_type": "markdown"
- },
- {
- "file": "meetups/2023/index.md",
- "text": "Ver detalles",
- "url": "202309-septiembre",
- "full_url": "http://127.0.0.1:8000/202309-septiembre",
+ "file": "about.md",
+ "text": "guía para ponentes",
+ "url": "comunidad/ponentes/",
+ "full_url": "http://127.0.0.1:8000/comunidad/ponentes/",
"status": "200 OK",
- "line": 19,
+ "line": 42,
"link_type": "markdown"
},
{
- "file": "meetups/2023/index.md",
- "text": "ponentes y voluntarios reconocidos",
- "url": "/comunidad/como-contribuir/",
- "full_url": "http://127.0.0.1:8000/comunidad/como-contribuir/",
+ "file": "about.md",
+ "text": "nuestra página de voluntarios",
+ "url": "comunidad/voluntarios/",
+ "full_url": "http://127.0.0.1:8000/comunidad/voluntarios/",
"status": "200 OK",
- "line": 59,
+ "line": 46,
"link_type": "markdown"
},
{
- "file": "meetups/2024/index.md",
+ "file": "meetups/2025/index.md",
"text": "Ver detalles",
- "url": "202411-noviembre",
- "full_url": "http://127.0.0.1:8000/202411-noviembre",
+ "url": "202508-agosto/",
+ "full_url": "http://127.0.0.1:8000/meetups/2025/202508-agosto",
"status": "200 OK",
"line": 15,
"link_type": "markdown"
},
{
- "file": "meetups/2024/index.md",
+ "file": "meetups/2025/index.md",
"text": "Ver detalles",
- "url": "202411-noviembre",
- "full_url": "http://127.0.0.1:8000/202411-noviembre",
+ "url": "202507-julio/",
+ "full_url": "http://127.0.0.1:8000/meetups/2025/202507-julio",
"status": "200 OK",
"line": 16,
"link_type": "markdown"
},
{
- "file": "meetups/2024/index.md",
+ "file": "meetups/2025/index.md",
"text": "Ver detalles",
- "url": "202410-octubre",
- "full_url": "http://127.0.0.1:8000/202410-octubre",
+ "url": "202506-junio/",
+ "full_url": "http://127.0.0.1:8000/meetups/2025/202506-junio",
"status": "200 OK",
"line": 17,
"link_type": "markdown"
},
{
- "file": "meetups/2024/index.md",
+ "file": "meetups/2025/index.md",
"text": "Ver detalles",
- "url": "202409-septiembre",
- "full_url": "http://127.0.0.1:8000/202409-septiembre",
+ "url": "202505-mayo/",
+ "full_url": "http://127.0.0.1:8000/meetups/2025/202505-mayo",
"status": "200 OK",
"line": 18,
"link_type": "markdown"
},
{
- "file": "meetups/2024/index.md",
+ "file": "meetups/2025/index.md",
"text": "Ver detalles",
- "url": "202409-septiembre",
- "full_url": "http://127.0.0.1:8000/202409-septiembre",
+ "url": "202504-unam/",
+ "full_url": "http://127.0.0.1:8000/meetups/2025/202504-unam",
"status": "200 OK",
"line": 19,
"link_type": "markdown"
},
{
- "file": "meetups/2024/index.md",
+ "file": "meetups/2025/index.md",
"text": "Ver detalles",
- "url": "202408-agosto",
- "full_url": "http://127.0.0.1:8000/202408-agosto",
+ "url": "202504-unam/",
+ "full_url": "http://127.0.0.1:8000/meetups/2025/202504-unam",
"status": "200 OK",
"line": 20,
"link_type": "markdown"
},
{
- "file": "meetups/2024/index.md",
+ "file": "meetups/2025/index.md",
"text": "Ver detalles",
- "url": "202408-agosto",
- "full_url": "http://127.0.0.1:8000/202408-agosto",
+ "url": "202504-abril/",
+ "full_url": "http://127.0.0.1:8000/meetups/2025/202504-abril",
"status": "200 OK",
"line": 21,
"link_type": "markdown"
},
{
- "file": "meetups/2024/index.md",
+ "file": "meetups/2025/index.md",
"text": "Ver detalles",
- "url": "202407-julio",
- "full_url": "http://127.0.0.1:8000/202407-julio",
+ "url": "202503-marzo/",
+ "full_url": "http://127.0.0.1:8000/meetups/2025/202503-marzo",
"status": "200 OK",
"line": 22,
"link_type": "markdown"
},
{
- "file": "meetups/2024/index.md",
+ "file": "meetups/2025/index.md",
"text": "Ver detalles",
- "url": "202407-julio",
- "full_url": "http://127.0.0.1:8000/202407-julio",
+ "url": "202503-marzo/",
+ "full_url": "http://127.0.0.1:8000/meetups/2025/202503-marzo",
"status": "200 OK",
"line": 23,
"link_type": "markdown"
},
{
- "file": "meetups/2024/index.md",
+ "file": "meetups/2025/index.md",
"text": "Ver detalles",
- "url": "202406-junio",
- "full_url": "http://127.0.0.1:8000/202406-junio",
+ "url": "202502-febrero/",
+ "full_url": "http://127.0.0.1:8000/meetups/2025/202502-febrero",
"status": "200 OK",
"line": 24,
"link_type": "markdown"
},
{
- "file": "meetups/2024/index.md",
+ "file": "meetups/2025/index.md",
"text": "Ver detalles",
- "url": "202405-mayo",
- "full_url": "http://127.0.0.1:8000/202405-mayo",
+ "url": "202502-febrero/",
+ "full_url": "http://127.0.0.1:8000/meetups/2025/202502-febrero",
"status": "200 OK",
"line": 25,
"link_type": "markdown"
},
{
- "file": "meetups/2024/index.md",
+ "file": "meetups/2025/index.md",
"text": "Ver detalles",
- "url": "202404-abril",
- "full_url": "http://127.0.0.1:8000/202404-abril",
+ "url": "202501-enero/",
+ "full_url": "http://127.0.0.1:8000/meetups/2025/202501-enero",
"status": "200 OK",
"line": 26,
"link_type": "markdown"
},
{
- "file": "meetups/2024/index.md",
+ "file": "meetups/2025/index.md",
"text": "Ver detalles",
- "url": "202403-marzo",
- "full_url": "http://127.0.0.1:8000/202403-marzo",
+ "url": "202501-enero/",
+ "full_url": "http://127.0.0.1:8000/meetups/2025/202501-enero",
"status": "200 OK",
"line": 27,
"link_type": "markdown"
},
{
- "file": "meetups/2024/index.md",
- "text": "Ver detalles",
- "url": "202402-febrero",
- "full_url": "http://127.0.0.1:8000/202402-febrero",
- "status": "200 OK",
- "line": 28,
- "link_type": "markdown"
- },
- {
- "file": "meetups/2024/index.md",
- "text": "Ver detalles",
- "url": "202401-enero",
- "full_url": "http://127.0.0.1:8000/202401-enero",
- "status": "200 OK",
- "line": 29,
- "link_type": "markdown"
- },
- {
- "file": "meetups/2024/index.md",
+ "file": "meetups/2025/index.md",
"text": "ponentes y voluntarios reconocidos",
- "url": "/comunidad/como-contribuir/",
- "full_url": "http://127.0.0.1:8000/comunidad/como-contribuir/",
+ "url": "../../comunidad/como-contribuir/",
+ "full_url": "http://127.0.0.1:8000/comunidad/como-contribuir",
"status": "200 OK",
- "line": 69,
+ "line": 68,
"link_type": "markdown"
},
{
- "file": "meetups/2025/index.md",
+ "file": "meetups/2024/index.md",
"text": "Ver detalles",
- "url": "202507-julio",
- "full_url": "http://127.0.0.1:8000/202507-julio",
+ "url": "202411-noviembre/",
+ "full_url": "http://127.0.0.1:8000/meetups/2024/202411-noviembre",
"status": "200 OK",
"line": 15,
"link_type": "markdown"
},
{
- "file": "meetups/2025/index.md",
+ "file": "meetups/2024/index.md",
"text": "Ver detalles",
- "url": "202506-junio",
- "full_url": "http://127.0.0.1:8000/202506-junio",
+ "url": "202411-noviembre/",
+ "full_url": "http://127.0.0.1:8000/meetups/2024/202411-noviembre",
"status": "200 OK",
"line": 16,
"link_type": "markdown"
},
{
- "file": "meetups/2025/index.md",
+ "file": "meetups/2024/index.md",
"text": "Ver detalles",
- "url": "202505-mayo",
- "full_url": "http://127.0.0.1:8000/202505-mayo",
+ "url": "202410-octubre/",
+ "full_url": "http://127.0.0.1:8000/meetups/2024/202410-octubre",
"status": "200 OK",
"line": 17,
"link_type": "markdown"
},
{
- "file": "meetups/2025/index.md",
+ "file": "meetups/2024/index.md",
"text": "Ver detalles",
- "url": "202504-abril",
- "full_url": "http://127.0.0.1:8000/202504-abril",
+ "url": "202409-septiembre/",
+ "full_url": "http://127.0.0.1:8000/meetups/2024/202409-septiembre",
"status": "200 OK",
"line": 18,
"link_type": "markdown"
},
{
- "file": "meetups/2025/index.md",
+ "file": "meetups/2024/index.md",
"text": "Ver detalles",
- "url": "202504-abril",
- "full_url": "http://127.0.0.1:8000/202504-abril",
+ "url": "202409-septiembre/",
+ "full_url": "http://127.0.0.1:8000/meetups/2024/202409-septiembre",
"status": "200 OK",
"line": 19,
"link_type": "markdown"
},
{
- "file": "meetups/2025/index.md",
+ "file": "meetups/2024/index.md",
"text": "Ver detalles",
- "url": "202504-abril",
- "full_url": "http://127.0.0.1:8000/202504-abril",
+ "url": "202408-agosto/",
+ "full_url": "http://127.0.0.1:8000/meetups/2024/202408-agosto",
"status": "200 OK",
"line": 20,
"link_type": "markdown"
},
{
- "file": "meetups/2025/index.md",
+ "file": "meetups/2024/index.md",
"text": "Ver detalles",
- "url": "202503-marzo",
- "full_url": "http://127.0.0.1:8000/202503-marzo",
+ "url": "202408-agosto/",
+ "full_url": "http://127.0.0.1:8000/meetups/2024/202408-agosto",
"status": "200 OK",
"line": 21,
"link_type": "markdown"
},
{
- "file": "meetups/2025/index.md",
+ "file": "meetups/2024/index.md",
"text": "Ver detalles",
- "url": "202503-marzo",
- "full_url": "http://127.0.0.1:8000/202503-marzo",
+ "url": "202407-julio/",
+ "full_url": "http://127.0.0.1:8000/meetups/2024/202407-julio",
"status": "200 OK",
"line": 22,
"link_type": "markdown"
},
{
- "file": "meetups/2025/index.md",
+ "file": "meetups/2024/index.md",
"text": "Ver detalles",
- "url": "202502-febrero",
- "full_url": "http://127.0.0.1:8000/202502-febrero",
+ "url": "202407-julio/",
+ "full_url": "http://127.0.0.1:8000/meetups/2024/202407-julio",
"status": "200 OK",
"line": 23,
"link_type": "markdown"
},
{
- "file": "meetups/2025/index.md",
+ "file": "meetups/2024/index.md",
"text": "Ver detalles",
- "url": "202502-febrero",
- "full_url": "http://127.0.0.1:8000/202502-febrero",
+ "url": "202406-junio/",
+ "full_url": "http://127.0.0.1:8000/meetups/2024/202406-junio",
"status": "200 OK",
"line": 24,
"link_type": "markdown"
},
{
- "file": "meetups/2025/index.md",
+ "file": "meetups/2024/index.md",
"text": "Ver detalles",
- "url": "202501-enero",
- "full_url": "http://127.0.0.1:8000/202501-enero",
+ "url": "202405-mayo/",
+ "full_url": "http://127.0.0.1:8000/meetups/2024/202405-mayo",
"status": "200 OK",
"line": 25,
"link_type": "markdown"
},
{
- "file": "meetups/2025/index.md",
+ "file": "meetups/2024/index.md",
"text": "Ver detalles",
- "url": "202501-enero",
- "full_url": "http://127.0.0.1:8000/202501-enero",
+ "url": "202404-abril/",
+ "full_url": "http://127.0.0.1:8000/meetups/2024/202404-abril",
"status": "200 OK",
"line": 26,
"link_type": "markdown"
},
{
- "file": "meetups/2025/index.md",
+ "file": "meetups/2024/index.md",
+ "text": "Ver detalles",
+ "url": "202403-marzo/",
+ "full_url": "http://127.0.0.1:8000/meetups/2024/202403-marzo",
+ "status": "200 OK",
+ "line": 27,
+ "link_type": "markdown"
+ },
+ {
+ "file": "meetups/2024/index.md",
+ "text": "Ver detalles",
+ "url": "202402-febrero/",
+ "full_url": "http://127.0.0.1:8000/meetups/2024/202402-febrero",
+ "status": "200 OK",
+ "line": 28,
+ "link_type": "markdown"
+ },
+ {
+ "file": "meetups/2024/index.md",
+ "text": "Ver detalles",
+ "url": "202401-enero/",
+ "full_url": "http://127.0.0.1:8000/meetups/2024/202401-enero",
+ "status": "200 OK",
+ "line": 29,
+ "link_type": "markdown"
+ },
+ {
+ "file": "meetups/2024/index.md",
"text": "ponentes y voluntarios reconocidos",
- "url": "/comunidad/como-contribuir/",
- "full_url": "http://127.0.0.1:8000/comunidad/como-contribuir/",
+ "url": "../../comunidad/como-contribuir/",
+ "full_url": "http://127.0.0.1:8000/comunidad/como-contribuir",
"status": "200 OK",
- "line": 67,
+ "line": 69,
"link_type": "markdown"
},
{
- "file": "comunidad/como-contribuir.md",
- "text": "Ser Ponente",
- "url": "ponentes/",
- "full_url": "http://127.0.0.1:8000/ponentes/",
+ "file": "meetups/2023/index.md",
+ "text": "Ver detalles",
+ "url": "202311-noviembre/",
+ "full_url": "http://127.0.0.1:8000/meetups/2023/202311-noviembre",
"status": "200 OK",
- "line": 31,
+ "line": 15,
"link_type": "markdown"
},
{
- "file": "comunidad/como-contribuir.md",
- "text": "Ser Voluntario",
- "url": "voluntarios/",
- "full_url": "http://127.0.0.1:8000/voluntarios/",
+ "file": "meetups/2023/index.md",
+ "text": "Ver detalles",
+ "url": "202311-noviembre/",
+ "full_url": "http://127.0.0.1:8000/meetups/2023/202311-noviembre",
"status": "200 OK",
- "line": 37,
+ "line": 16,
"link_type": "markdown"
},
{
- "file": "comunidad/como-contribuir.md",
- "text": "Alianzas",
- "url": "alianzas/",
- "full_url": "http://127.0.0.1:8000/alianzas/",
+ "file": "meetups/2023/index.md",
+ "text": "Ver detalles",
+ "url": "202310-octubre/",
+ "full_url": "http://127.0.0.1:8000/meetups/2023/202310-octubre",
"status": "200 OK",
- "line": 55,
+ "line": 17,
"link_type": "markdown"
},
{
- "file": "comunidad/como-contribuir.md",
- "text": "formas de contribuir",
- "url": "#formas-de-contribuir",
- "full_url": "http://127.0.0.1:8000/comunidad/como-contribuir/#formas-de-contribuir",
+ "file": "meetups/2023/index.md",
+ "text": "Ver detalles",
+ "url": "202309-septiembre/",
+ "full_url": "http://127.0.0.1:8000/meetups/2023/202309-septiembre",
"status": "200 OK",
- "line": 63,
+ "line": 18,
"link_type": "markdown"
},
{
- "file": "comunidad/como-contribuir.md",
- "text": "nuestros meetups",
- "url": "../meetups/",
- "full_url": "http://127.0.0.1:8000/meetups/",
+ "file": "meetups/2023/index.md",
+ "text": "Ver detalles",
+ "url": "202309-septiembre/",
+ "full_url": "http://127.0.0.1:8000/meetups/2023/202309-septiembre",
"status": "200 OK",
- "line": 70,
+ "line": 19,
+ "link_type": "markdown"
+ },
+ {
+ "file": "meetups/2023/index.md",
+ "text": "ponentes y voluntarios reconocidos",
+ "url": "../../comunidad/como-contribuir/",
+ "full_url": "http://127.0.0.1:8000/comunidad/como-contribuir",
+ "status": "200 OK",
+ "line": 59,
"link_type": "markdown"
},
{
@@ -713,10 +696,55 @@
"file": "comunidad/voluntarios.md",
"text": "Más Información",
"url": "como-contribuir/",
- "full_url": "http://127.0.0.1:8000/como-contribuir/",
+ "full_url": "http://127.0.0.1:8000/comunidad/como-contribuir",
"status": "200 OK",
"line": 87,
"link_type": "html"
+ },
+ {
+ "file": "comunidad/sedes_faq.md",
+ "text": "Ser Ponente",
+ "url": "ponentes/#por-que-ser-ponente",
+ "full_url": "http://127.0.0.1:8000/comunidad/ponentes/#por-que-ser-ponente",
+ "status": "200 OK",
+ "line": 109,
+ "link_type": "markdown"
+ },
+ {
+ "file": "comunidad/como-contribuir.md",
+ "text": "Ser Ponente",
+ "url": "ponentes/",
+ "full_url": "http://127.0.0.1:8000/comunidad/ponentes",
+ "status": "200 OK",
+ "line": 31,
+ "link_type": "markdown"
+ },
+ {
+ "file": "comunidad/como-contribuir.md",
+ "text": "Ser Voluntario",
+ "url": "voluntarios/",
+ "full_url": "http://127.0.0.1:8000/comunidad/voluntarios",
+ "status": "200 OK",
+ "line": 37,
+ "link_type": "markdown"
+ },
+ {
+ "file": "comunidad/como-contribuir.md",
+ "text": "Alianzas",
+ "url": "alianzas/",
+ "full_url": "http://127.0.0.1:8000/comunidad/alianzas",
+ "status": "200 OK",
+ "line": 55,
+ "link_type": "markdown"
+ },
+ {
+ "file": "comunidad/como-contribuir.md",
+ "text": "nuestros meetups",
+ "url": "../meetups/index.md",
+ "full_url": "http://127.0.0.1:8000/meetups/index.html",
+ "status": "200 OK",
+ "line": 70,
+ "link_type": "markdown"
}
]
-}
+}
\ No newline at end of file
diff --git a/docs/about.md b/docs/about.md
index c50b71d..507aeb0 100644
--- a/docs/about.md
+++ b/docs/about.md
@@ -39,11 +39,11 @@ Creemos que el verdadero crecimiento profesional viene de la colaboración y el
###
@@ -36,12 +36,12 @@
Ser Ponente
Comparte tu conocimiento con la comunidad. Charlas técnicas, casos de uso, mejores prácticas y más.
-
Conoce Más
+
Conoce Más
Ser Voluntario
Ayuda a organizar eventos, gestionar redes sociales, o contribuir con el desarrollo del sitio web.
-
Únete
+
Únete
Asistir
diff --git a/docs/js/custom.js b/docs/js/custom.js
index 43c71e6..fec0ecd 100644
--- a/docs/js/custom.js
+++ b/docs/js/custom.js
@@ -18,75 +18,7 @@ document.addEventListener('DOMContentLoaded', function() {
imageObserver.observe(img);
});
- // Búsqueda avanzada
- const searchInput = document.querySelector('.search-input');
- const searchFilters = document.querySelectorAll('.filter-chip');
- const searchableCards = document.querySelectorAll('.speaker-card, .volunteer-card, .meetup-card');
-
- if (searchInput) {
- searchInput.addEventListener('input', function() {
- const searchTerm = this.value.toLowerCase();
- filterCards(searchTerm, getActiveFilters());
- });
- }
-
- searchFilters.forEach(filter => {
- filter.addEventListener('click', function() {
- this.classList.toggle('active');
- const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
- filterCards(searchTerm, getActiveFilters());
- });
- });
-
- function getActiveFilters() {
- return Array.from(document.querySelectorAll('.filter-chip.active'))
- .map(filter => filter.textContent.toLowerCase());
- }
-
- function filterCards(searchTerm, activeFilters) {
- searchableCards.forEach(card => {
- const cardText = card.textContent.toLowerCase();
- const cardTags = Array.from(card.querySelectorAll('.badge'))
- .map(badge => badge.textContent.toLowerCase());
-
- const matchesSearch = searchTerm === '' || cardText.includes(searchTerm);
- const matchesFilters = activeFilters.length === 0 ||
- activeFilters.some(filter => cardTags.includes(filter));
-
- if (matchesSearch && matchesFilters) {
- card.style.display = 'block';
- card.classList.remove('hidden');
- } else {
- card.style.display = 'none';
- card.classList.add('hidden');
- }
- });
-
- updateSearchResults();
- }
-
- function updateSearchResults() {
- const visibleCards = document.querySelectorAll('.speaker-card:not(.hidden), .volunteer-card:not(.hidden), .meetup-card:not(.hidden)');
- const resultsCount = document.querySelector('.search-results-count');
-
- if (resultsCount) {
- resultsCount.textContent = `${visibleCards.length} resultados encontrados`;
- }
- }
-
- // Botón de limpiar filtros
- const clearFiltersBtn = document.querySelector('.clear-filters');
- if (clearFiltersBtn) {
- clearFiltersBtn.addEventListener('click', function() {
- searchFilters.forEach(filter => filter.classList.remove('active'));
- if (searchInput) {
- searchInput.value = '';
- }
- filterCards('', []);
- });
- }
-
- // Animaciones suaves para las tarjetas
+ // Animaciones suaves para las tarjetas de voluntarios
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
@@ -101,7 +33,8 @@ document.addEventListener('DOMContentLoaded', function() {
});
}, observerOptions);
- searchableCards.forEach(card => {
+ const volunteerCards = document.querySelectorAll('.volunteer-card');
+ volunteerCards.forEach(card => {
card.style.opacity = '0';
card.style.transform = 'translateY(20px)';
card.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
@@ -165,20 +98,6 @@ document.addEventListener('DOMContentLoaded', function() {
wrapper.appendChild(table);
});
- // Mejoras de formularios
- const formInputs = document.querySelectorAll('input, textarea, select');
- formInputs.forEach(input => {
- input.addEventListener('focus', function() {
- this.parentElement.classList.add('focused');
- });
-
- input.addEventListener('blur', function() {
- if (!this.value) {
- this.parentElement.classList.remove('focused');
- }
- });
- });
-
// Botones de acción mejorados
const actionButtons = document.querySelectorAll('.btn-action');
actionButtons.forEach(button => {
@@ -203,36 +122,14 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
- // Mejoras de breadcrumbs
- const breadcrumbLinks = document.querySelectorAll('.breadcrumb-item a');
- breadcrumbLinks.forEach(link => {
- link.addEventListener('click', function(e) {
- // Añadir indicador de navegación
- this.style.position = 'relative';
- const indicator = document.createElement('span');
- indicator.style.position = 'absolute';
- indicator.style.bottom = '-2px';
- indicator.style.left = '0';
- indicator.style.width = '0';
- indicator.style.height = '2px';
- indicator.style.backgroundColor = 'var(--python-green)';
- indicator.style.transition = 'width 0.3s ease';
- this.appendChild(indicator);
-
- setTimeout(() => {
- indicator.style.width = '100%';
- }, 100);
- });
- });
-
- // Optimización de rendimiento
+ // Optimización de rendimiento en resize
let resizeTimer;
window.addEventListener('resize', function() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function() {
// Recalcular posiciones después del resize
cardObserver.disconnect();
- searchableCards.forEach(card => {
+ volunteerCards.forEach(card => {
cardObserver.observe(card);
});
}, 250);
@@ -269,18 +166,8 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
- // Mejoras de carga de página
+ // Animar elementos de la página al cargar
window.addEventListener('load', function() {
- // Ocultar loader si existe
- const loader = document.querySelector('.page-loader');
- if (loader) {
- loader.style.opacity = '0';
- setTimeout(() => {
- loader.style.display = 'none';
- }, 300);
- }
-
- // Animar elementos de la página
const animatedElements = document.querySelectorAll('.hero-section, .stats-grid, .features-grid');
animatedElements.forEach((element, index) => {
setTimeout(() => {
@@ -290,37 +177,6 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
- // Mejoras de SEO y analytics
- const trackEvent = function(category, action, label) {
- if (typeof gtag !== 'undefined') {
- gtag('event', action, {
- 'event_category': category,
- 'event_label': label
- });
- }
- };
-
- // Track clicks en botones de acción
- actionButtons.forEach(button => {
- button.addEventListener('click', function() {
- const action = this.textContent.trim();
- trackEvent('engagement', 'button_click', action);
- });
- });
-
- // Track búsquedas
- if (searchInput) {
- let searchTimeout;
- searchInput.addEventListener('input', function() {
- clearTimeout(searchTimeout);
- searchTimeout = setTimeout(() => {
- if (this.value.length > 2) {
- trackEvent('search', 'search_performed', this.value);
- }
- }, 1000);
- });
- }
-
// Mejoras de UX para móviles
if (window.innerWidth <= 768) {
// Optimizar navegación móvil
@@ -352,7 +208,7 @@ document.addEventListener('DOMContentLoaded', function() {
console.log('Python CDMX Custom JavaScript loaded successfully!');
});
-// Estilos CSS adicionales para las mejoras de JavaScript
+// Estilos CSS para los efectos de JavaScript
const additionalStyles = `
.button-ripple {
position: absolute;
@@ -379,35 +235,10 @@ const additionalStyles = `
pointer-events: none;
}
- .search-results-count {
- text-align: center;
- color: var(--python-gray);
- font-size: 0.9rem;
- margin: 1rem 0;
- }
-
- .page-loader {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: white;
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 9999;
- transition: opacity 0.3s ease;
- }
-
.scrolling * {
animation-play-state: paused !important;
}
- .focused {
- transform: translateY(-2px);
- }
-
@media (max-width: 768px) {
.button-ripple {
display: none;
diff --git a/docs/meetups/2023/index.md b/docs/meetups/2023/index.md
index 1aa5b85..8a3c9de 100644
--- a/docs/meetups/2023/index.md
+++ b/docs/meetups/2023/index.md
@@ -12,11 +12,11 @@
| **Fecha** | **Charla** | **Ponente** | **Lugar** | **Detalles** |
|:---|:---|:---|:---|:---|
-| **14 Noviembre 2023** | GitOps: Automatizando el despliegue de aplicaciones | Carlos Reyes | Wizeline México | [Ver detalles](202311-noviembre.md) |
-| **14 Noviembre 2023** | Historia de Python: De Guido van Rossum a la actualidad | Gustavo Vera | Wizeline México | [Ver detalles](202311-noviembre.md) |
-| **10 Octubre 2023** | Jupyter a Web: De notebooks a aplicaciones web | Gustavo Vera | Wizeline México | [Ver detalles](202310-octubre.md) |
-| **12 Septiembre 2023** | Metaprogramación en Python | David Sol | Wizeline México | [Ver detalles](202309-septiembre.md) |
-| **12 Septiembre 2023** | AWS AI: Servicios de IA en la nube | Gustavo Vera | Wizeline México | [Ver detalles](202309-septiembre.md) |
+| **14 Noviembre 2023** | GitOps: Automatizando el despliegue de aplicaciones | Carlos Reyes | Wizeline México | [Ver detalles](202311-noviembre/) |
+| **14 Noviembre 2023** | Historia de Python: De Guido van Rossum a la actualidad | Gustavo Vera | Wizeline México | [Ver detalles](202311-noviembre/) |
+| **10 Octubre 2023** | Jupyter a Web: De notebooks a aplicaciones web | Gustavo Vera | Wizeline México | [Ver detalles](202310-octubre/) |
+| **12 Septiembre 2023** | Metaprogramación en Python | David Sol | Wizeline México | [Ver detalles](202309-septiembre/) |
+| **12 Septiembre 2023** | AWS AI: Servicios de IA en la nube | Gustavo Vera | Wizeline México | [Ver detalles](202309-septiembre/) |
---
@@ -56,4 +56,4 @@
---
-¿Te gustaría aparecer aquí? Conoce a nuestros [ponentes y voluntarios reconocidos](../../comunidad/como-contribuir.md).
+¿Te gustaría aparecer aquí? Conoce a nuestros [ponentes y voluntarios reconocidos](../../comunidad/como-contribuir/).
diff --git a/docs/meetups/2024/index.md b/docs/meetups/2024/index.md
index 666e809..39b65bf 100644
--- a/docs/meetups/2024/index.md
+++ b/docs/meetups/2024/index.md
@@ -12,21 +12,21 @@
| **Fecha** | **Charla** | **Ponente** | **Lugar** | **Detalles** |
|:---|:---|:---|:---|:---|
-| **12 Noviembre 2024** | Mejora tu código: Evita anti-patrones en Python | Alejandro Lopez | Wizeline México | [Ver detalles](202411-noviembre.md) |
-| **12 Noviembre 2024** | Exactamente qué y (sobre todo) por qué ChatGPT | Hugo Ramírez | Wizeline México | [Ver detalles](202411-noviembre.md) |
-| **08 Octubre 2024** | Contribuir a Open Source | Alex Callejas | Wizeline México | [Ver detalles](202410-octubre.md) |
-| **10 Septiembre 2024** | Protocolos en Python | Diego Barriga | Wizeline México | [Ver detalles](202409-septiembre.md) |
-| **10 Septiembre 2024** | Guía Open Source | David Sol | Wizeline México | [Ver detalles](202409-septiembre.md) |
-| **13 Agosto 2024** | ETLs con Python | Gustavo Vera | Wizeline México | [Ver detalles](202408-agosto.md) |
-| **13 Agosto 2024** | GIL: Global Interpreter Lock | Fer Perales | Wizeline México | [Ver detalles](202408-agosto.md) |
-| **09 Julio 2024** | Regresión Lineal | Konstantin Spirin | Wizeline México | [Ver detalles](202407-julio.md) |
-| **09 Julio 2024** | Fonética con Python | Hugo Ramirez | Wizeline México | [Ver detalles](202407-julio.md) |
-| **11 Junio 2024** | Regresión Lineal | Geovanni Zepeda Martínez | Wizeline México | [Ver detalles](202406-junio.md) |
-| **14 Mayo 2024** | Pydantic | Charly Román | Wizeline México | [Ver detalles](202405-mayo.md) |
-| **09 Abril 2024** | Contenedores | David Sol | Wizeline México | [Ver detalles](202404-abril.md) |
-| **12 Marzo 2024** | Flask APIs | Alejandro López | Wizeline México | [Ver detalles](202403-marzo.md) |
-| **13 Febrero 2024** | Entornos Virtuales | Gustavo Vera | Wizeline México | [Ver detalles](202402-febrero.md) |
-| **09 Enero 2024** | PyPI | David Sol | Wizeline México | [Ver detalles](202401-enero.md) |
+| **12 Noviembre 2024** | Mejora tu código: Evita anti-patrones en Python | Alejandro Lopez | Wizeline México | [Ver detalles](202411-noviembre/) |
+| **12 Noviembre 2024** | Exactamente qué y (sobre todo) por qué ChatGPT | Hugo Ramírez | Wizeline México | [Ver detalles](202411-noviembre/) |
+| **08 Octubre 2024** | Contribuir a Open Source | Alex Callejas | Wizeline México | [Ver detalles](202410-octubre/) |
+| **10 Septiembre 2024** | Protocolos en Python | Diego Barriga | Wizeline México | [Ver detalles](202409-septiembre/) |
+| **10 Septiembre 2024** | Guía Open Source | David Sol | Wizeline México | [Ver detalles](202409-septiembre/) |
+| **13 Agosto 2024** | ETLs con Python | Gustavo Vera | Wizeline México | [Ver detalles](202408-agosto/) |
+| **13 Agosto 2024** | GIL: Global Interpreter Lock | Fer Perales | Wizeline México | [Ver detalles](202408-agosto/) |
+| **09 Julio 2024** | Regresión Lineal | Konstantin Spirin | Wizeline México | [Ver detalles](202407-julio/) |
+| **09 Julio 2024** | Fonética con Python | Hugo Ramirez | Wizeline México | [Ver detalles](202407-julio/) |
+| **11 Junio 2024** | Regresión Lineal | Geovanni Zepeda Martínez | Wizeline México | [Ver detalles](202406-junio/) |
+| **14 Mayo 2024** | Pydantic | Charly Román | Wizeline México | [Ver detalles](202405-mayo/) |
+| **09 Abril 2024** | Contenedores | David Sol | Wizeline México | [Ver detalles](202404-abril/) |
+| **12 Marzo 2024** | Flask APIs | Alejandro López | Wizeline México | [Ver detalles](202403-marzo/) |
+| **13 Febrero 2024** | Entornos Virtuales | Gustavo Vera | Wizeline México | [Ver detalles](202402-febrero/) |
+| **09 Enero 2024** | PyPI | David Sol | Wizeline México | [Ver detalles](202401-enero/) |
---
@@ -66,4 +66,4 @@
---
-¿Te gustaría aparecer aquí? Conoce a nuestros [ponentes y voluntarios reconocidos](../../comunidad/como-contribuir.md).
+¿Te gustaría aparecer aquí? Conoce a nuestros [ponentes y voluntarios reconocidos](../../comunidad/como-contribuir/).
diff --git a/docs/meetups/2025/index.md b/docs/meetups/2025/index.md
index b923d64..f3b5ee1 100644
--- a/docs/meetups/2025/index.md
+++ b/docs/meetups/2025/index.md
@@ -12,19 +12,19 @@
| **Fecha** | **Charla** | **Ponente** | **Lugar** | **Detalles** |
|:---|:---|:---|:---|:---|
-| **12 Agosto 2025** | Cómo preparar una ambiente de desarrollo con Python desde zero | Juan Guillermo Gómez | Jardin Chapultepec | [Ver detalles](202508-agosto.md) |
-| **08 Julio 2025** | Cómo preparar una ambiente de desarrollo con Python desde zero | David Sol | Clara | [Ver detalles](202507-julio.md) |
-| **10 Junio 2025** | Usando Python y software libre para crear nuevas herramientas: Traductor de voz español-inglés | Carlos Cesar Caballero | Wizeline México | [Ver detalles](202506-junio.md) |
-| **13 Mayo 2025** | Construyendo un paquete en Python y publicándolo en PyPI | Javier Novoa | Wizeline México | [Ver detalles](202505-mayo.md) |
-| **25 Abril 2025** | portafolio.py: Como hacer un portafolio web sin saber diseño web | Daniel Paredes | UNAM Facultad de Ciencias | [Ver detalles](202504-unam.md) |
-| **25 Abril 2025** | Programar en tiempos del Vibe-Coding | Charly Roman | UNAM Facultad de Ciencias | [Ver detalles](202504-unam.md) |
-| **08 Abril 2025** | El para que cosa de Quien. Kubernetes y AI | Carlos Reyes | Wizeline México | [Ver detalles](202504-abril.md) |
-| **11 Marzo 2025** | Mi Primer Agente de Inteligencia Artificial con Python | Erik Rivera | Wizeline México | [Ver detalles](202503-marzo.md) |
-| **11 Maro 2025** | Interfases gráficas con Pyside6 | David Sol | Wizeline México | [Ver detalles](202503-marzo.md) |
-| **11 Febrero 2025** | Lecciones del Advent of Code 2024 | Manuel Rábade | Wizeline México | [Ver detalles](202502-febrero.md) |
-| **11 Febrero 2025** | Embeddings: El lenguaje como las máquinas entienden el lenguaje humano | Juan Guillermo Gómez | Wizeline México | [Ver detalles](202502-febrero.md) |
-| **14 Enero 2025** | Crea extensiones para LibreOffice con Python | elMau (Mauricio B.) | Wizeline México | [Ver detalles](202501-enero.md) |
-| **14 Enero 2025** | Seguridad y cumplimiento de Python: Garantizar el cumplimiento de PCI DSS | Mauro Parra | Wizeline México | [Ver detalles](202501-enero.md) |
+| **12 Agosto 2025** | Cómo preparar una ambiente de desarrollo con Python desde zero | Juan Guillermo Gómez | Jardin Chapultepec | [Ver detalles](202508-agosto/) |
+| **08 Julio 2025** | Cómo preparar una ambiente de desarrollo con Python desde zero | David Sol | Clara | [Ver detalles](202507-julio/) |
+| **10 Junio 2025** | Usando Python y software libre para crear nuevas herramientas: Traductor de voz español-inglés | Carlos Cesar Caballero | Wizeline México | [Ver detalles](202506-junio/) |
+| **13 Mayo 2025** | Construyendo un paquete en Python y publicándolo en PyPI | Javier Novoa | Wizeline México | [Ver detalles](202505-mayo/) |
+| **25 Abril 2025** | portafolio.py: Como hacer un portafolio web sin saber diseño web | Daniel Paredes | UNAM Facultad de Ciencias | [Ver detalles](202504-unam/) |
+| **25 Abril 2025** | Programar en tiempos del Vibe-Coding | Charly Roman | UNAM Facultad de Ciencias | [Ver detalles](202504-unam/) |
+| **08 Abril 2025** | El para que cosa de Quien. Kubernetes y AI | Carlos Reyes | Wizeline México | [Ver detalles](202504-abril/) |
+| **11 Marzo 2025** | Mi Primer Agente de Inteligencia Artificial con Python | Erik Rivera | Wizeline México | [Ver detalles](202503-marzo/) |
+| **11 Maro 2025** | Interfases gráficas con Pyside6 | David Sol | Wizeline México | [Ver detalles](202503-marzo/) |
+| **11 Febrero 2025** | Lecciones del Advent of Code 2024 | Manuel Rábade | Wizeline México | [Ver detalles](202502-febrero/) |
+| **11 Febrero 2025** | Embeddings: El lenguaje como las máquinas entienden el lenguaje humano | Juan Guillermo Gómez | Wizeline México | [Ver detalles](202502-febrero/) |
+| **14 Enero 2025** | Crea extensiones para LibreOffice con Python | elMau (Mauricio B.) | Wizeline México | [Ver detalles](202501-enero/) |
+| **14 Enero 2025** | Seguridad y cumplimiento de Python: Garantizar el cumplimiento de PCI DSS | Mauro Parra | Wizeline México | [Ver detalles](202501-enero/) |
---
@@ -65,4 +65,4 @@
---
-¿Te gustaría aparecer aquí? Conoce a nuestros [ponentes y voluntarios reconocidos](../../comunidad/como-contribuir.md).
+¿Te gustaría aparecer aquí? Conoce a nuestros [ponentes y voluntarios reconocidos](../../comunidad/como-contribuir/).
diff --git a/presnetacion.md b/docs/templates/presentaciones/2025-06-junio.md
similarity index 100%
rename from presnetacion.md
rename to docs/templates/presentaciones/2025-06-junio.md
diff --git a/terraform/.gitignore b/terraform/.gitignore
new file mode 100644
index 0000000..15b2fcb
--- /dev/null
+++ b/terraform/.gitignore
@@ -0,0 +1,33 @@
+# Local .terraform directories
+**/.terraform/*
+
+# .tfstate files
+*.tfstate
+*.tfstate.*
+
+# Crash log files
+crash.log
+crash.*.log
+
+# Exclude all .tfvars files, which are likely to contain sensitive data
+*.tfvars
+*.tfvars.json
+
+# Ignore override files as they are usually used to override resources locally
+override.tf
+override.tf.json
+*_override.tf
+*_override.tf.json
+
+# Include override files you do wish to add to version control using negation pattern
+# !example_override.tf
+
+# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
+*tfplan*
+
+# Ignore CLI configuration files
+.terraformrc
+terraform.rc
+
+# Lock files (commit these to ensure consistent provider versions)
+# .terraform.lock.hcl
diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl
new file mode 100644
index 0000000..cdc1668
--- /dev/null
+++ b/terraform/.terraform.lock.hcl
@@ -0,0 +1,25 @@
+# This file is maintained automatically by "terraform init".
+# Manual edits may be lost in future updates.
+
+provider "registry.terraform.io/hashicorp/aws" {
+ version = "5.100.0"
+ constraints = "~> 5.0"
+ hashes = [
+ "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=",
+ "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644",
+ "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2",
+ "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274",
+ "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b",
+ "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862",
+ "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342",
+ "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
+ "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93",
+ "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2",
+ "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e",
+ "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421",
+ "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4",
+ "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9",
+ "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9",
+ "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70",
+ ]
+}
diff --git a/terraform/README.md b/terraform/README.md
new file mode 100644
index 0000000..8b99777
--- /dev/null
+++ b/terraform/README.md
@@ -0,0 +1,474 @@
+# Python CDMX - Terraform Infrastructure
+
+Este directorio contiene la infraestructura como código (IaC) para el sitio web de Python CDMX usando AWS.
+
+## 📋 Arquitectura
+
+```
+ ┌──────────────────────┐
+ │ Route53 Hosted │
+ │ pythoncdmx.org │
+ └──────────┬───────────┘
+ │
+ ┌──────────────┴──────────────┐
+ │ │
+ ┌───────────▼──────────┐ ┌───────────▼──────────┐
+ │ pythoncdmx.org │ │ staging.pythoncdmx.org│
+ │ www.pythoncdmx.org │ │ (Testing) │
+ └───────────┬──────────┘ └───────────┬───────────┘
+ │ │
+ │ HTTPS (TLS 1.2+) │ HTTPS (TLS 1.2+)
+ │ │
+ ▼ ▼
+ ┌─────────────────────┐ ┌─────────────────────┐
+ │ CloudFront PROD │ │ CloudFront STAGING │
+ │ - ACM Certificate │ │ - ACM Certificate │
+ │ - Cache: 1h-24h │ │ - Cache: 5min-2h │
+ │ - Gzip/Brotli │ │ - Shorter TTL │
+ └──────────┬──────────┘ └──────────┬──────────┘
+ │ │
+ │ OAC (SigV4) │ OAC (SigV4)
+ │ │
+ ▼ ▼
+ ┌──────────────────────┐ ┌──────────────────────┐
+ │ S3: pythoncdmx- │ │ S3: pythoncdmx- │
+ │ website (PROD) │ │ website-staging │
+ │ - Private │ │ - Private │
+ │ - Versioning │ │ - Versioning │
+ │ - AES256 │ │ - AES256 │
+ └──────────────────────┘ └──────────────────────┘
+```
+
+## 🚀 Recursos Creados
+
+### 🌐 Route53 (DNS)
+- **Hosted Zone**: pythoncdmx.org (existente, solo se agregan registros)
+- **Registros Production**:
+ - A/AAAA: `pythoncdmx.org` → CloudFront PROD
+ - A/AAAA: `www.pythoncdmx.org` → CloudFront PROD
+- **Registros Staging**:
+ - A/AAAA: `staging.pythoncdmx.org` → CloudFront STAGING
+- **Validación ACM**: Registros CNAME automáticos para certificados
+
+### 📦 Production Environment
+
+#### 1. **S3 Bucket (Production)**
+- **Nombre**: `pythoncdmx-website`
+- **Acceso**: Privado (solo CloudFront puede acceder)
+- **Características**:
+ - ✅ Versioning habilitado
+ - ✅ Encriptación AES256
+ - ✅ Lifecycle: 90 días versiones antiguas
+ - ✅ CORS configurado
+
+#### 2. **CloudFront Distribution (Production)**
+- **Dominios**: pythoncdmx.org, www.pythoncdmx.org
+- **Características**:
+ - ✅ Certificado SSL/TLS (ACM)
+ - ✅ HTTP/2 y HTTP/3 habilitado
+ - ✅ Compresión Gzip/Brotli
+ - ✅ Cache optimizado (HTML: 10min, Assets: 24h, Images: 7d)
+ - ✅ IPv6 habilitado
+ - ✅ Origin Access Control (OAC)
+
+#### 3. **ACM Certificate (Production)**
+- **Región**: us-east-1 (requerido para CloudFront)
+- **Validación**: DNS automática vía Route53
+- **Dominios cubiertos**:
+ - pythoncdmx.org
+ - www.pythoncdmx.org
+
+### 🧪 Staging Environment
+
+#### 4. **S3 Bucket (Staging)**
+- **Nombre**: `pythoncdmx-website-staging`
+- **Acceso**: Privado (solo CloudFront puede acceder)
+- **Características**:
+ - ✅ Versioning habilitado
+ - ✅ Encriptación AES256
+ - ✅ Lifecycle: 30 días versiones antiguas (más agresivo)
+ - ✅ CORS configurado
+
+#### 5. **CloudFront Distribution (Staging)**
+- **Dominio**: staging.pythoncdmx.org
+- **Características**:
+ - ✅ Certificado SSL/TLS (ACM)
+ - ✅ Cache más corto (HTML: 1min, Assets: 30min, Images: 1h)
+ - ✅ IPv6 habilitado
+ - ✅ Banner "STAGING" en todas las páginas
+
+#### 6. **ACM Certificate (Staging)**
+- **Región**: us-east-1
+- **Validación**: DNS automática vía Route53
+- **Dominio**: staging.pythoncdmx.org
+
+### 🔒 Security & State
+
+#### 7. **IAM Role (GitHub Actions)**
+- **OIDC Provider**: GitHub Actions sin credenciales long-lived
+- **Permisos**:
+ - S3: Read/Write en ambos buckets (prod y staging)
+ - CloudFront: Invalidación de cache en ambas distribuciones
+ - Scope: Repositorio PythonMexico/pythonCDMX
+
+#### 8. **Backend State**
+- **S3 Bucket**: `pythoncdmx-terraform-state`
+- **DynamoDB**: `pythoncdmx-terraform-locks`
+- **Encriptación**: Habilitada
+
+## 📦 Prerequisitos
+
+1. **Terraform** >= 1.0
+ ```bash
+ brew install terraform # macOS
+ ```
+
+2. **AWS CLI** configurado
+ ```bash
+ aws configure
+ ```
+
+3. **Route53 Hosted Zone** ya creada para `pythoncdmx.org`
+ ```bash
+ # Verificar hosted zone existente
+ aws route53 list-hosted-zones
+ ```
+
+4. **Permisos AWS requeridos**:
+ - S3: Crear/modificar buckets
+ - CloudFront: Crear/modificar distribuciones
+ - ACM: Solicitar/validar certificados
+ - Route53: Crear/modificar registros DNS
+ - IAM: Crear roles y políticas
+
+## 🔧 Configuración Inicial
+
+### 1. Crear Backend de Terraform (Una sola vez)
+
+```bash
+# Crear bucket para Terraform state
+aws s3 mb s3://pythoncdmx-terraform-state --region us-east-1
+
+# Habilitar versioning
+aws s3api put-bucket-versioning \
+ --bucket pythoncdmx-terraform-state \
+ --versioning-configuration Status=Enabled
+
+# Habilitar encriptación
+aws s3api put-bucket-encryption \
+ --bucket pythoncdmx-terraform-state \
+ --server-side-encryption-configuration '{
+ "Rules": [{
+ "ApplyServerSideEncryptionByDefault": {
+ "SSEAlgorithm": "AES256"
+ }
+ }]
+ }'
+
+# Crear tabla DynamoDB para locks
+aws dynamodb create-table \
+ --table-name pythoncdmx-terraform-locks \
+ --attribute-definitions AttributeName=LockID,AttributeType=S \
+ --key-schema AttributeName=LockID,KeyType=HASH \
+ --billing-mode PAY_PER_REQUEST \
+ --region us-east-1
+```
+
+### 2. Validar Dominio en ACM
+
+**✅ AUTOMÁTICO**: La validación DNS se hace automáticamente vía Route53. Terraform crea los registros CNAME necesarios y espera la validación.
+
+Si necesitas verificar el estado:
+
+```bash
+# Ver certificados y su estado
+aws acm list-certificates --region us-east-1
+
+# Ver detalles de validación
+terraform output certificate_validation_records
+```
+
+**Tiempo de validación**: 5-30 minutos (automático)
+
+## 🏗️ Despliegue
+
+### Inicializar Terraform
+
+```bash
+cd terraform
+terraform init
+```
+
+### Plan de Cambios
+
+```bash
+terraform plan
+```
+
+### Aplicar Infraestructura
+
+```bash
+terraform apply
+```
+
+Terraform creará:
+- ✅ 2 S3 buckets privados (production + staging)
+- ✅ 2 CloudFront distributions
+- ✅ 2 Certificados ACM (validación DNS automática)
+- ✅ Registros DNS en Route53
+- ✅ IAM Role con OIDC para GitHub Actions
+- ✅ Políticas de acceso
+- ✅ Cache behaviors optimizados
+
+**⏱️ Tiempo estimado**: 15-30 minutos (mayoría es validación de certificados)
+
+### Obtener Outputs
+
+```bash
+terraform output
+```
+
+Outputs importantes:
+- **Production**:
+ - `website_url`: https://pythoncdmx.org
+ - `cloudfront_distribution_id`: ID para invalidación de cache
+ - `website_bucket_name`: pythoncdmx-website
+- **Staging**:
+ - `staging_website_url`: https://staging.pythoncdmx.org
+ - `staging_cloudfront_distribution_id`: ID para invalidación de cache staging
+ - `staging_bucket_name`: pythoncdmx-website-staging
+- **Route53**:
+ - `hosted_zone_id`: ID de la hosted zone
+ - `hosted_zone_name_servers`: Name servers (para verificación)
+
+## 🔐 Configuración de GitHub Actions
+
+El deploy automático requiere configurar secretos en GitHub:
+
+### 1. Crear IAM Role para GitHub OIDC
+
+```bash
+# Ver terraform/iam-github.tf (crear este archivo)
+terraform apply
+```
+
+### 2. Configurar Secrets en GitHub
+
+En el repositorio, ir a **Settings > Secrets and variables > Actions**:
+
+```bash
+# Obtener valores de Terraform
+terraform output
+
+# Configurar estos secrets:
+AWS_ROLE_ARN: arn:aws:iam::123456789012:role/GitHubActionsDeployRole
+CLOUDFRONT_DISTRIBUTION_ID: E1234ABCDEF567 (production)
+CLOUDFRONT_DISTRIBUTION_ID_STAGING: E7890GHIJKL123 (staging)
+```
+
+### 3. Workflows Configurados
+
+**Production** (`.github/workflows/deploy-aws.yml`):
+- **Trigger**: Push a `main`
+- **Destino**: pythoncdmx.org
+- **S3 Bucket**: pythoncdmx-website
+- **Cache**: Agresivo (1h-24h)
+
+**Staging** (`.github/workflows/deploy-staging.yml`):
+- **Trigger**: Push a `develop`/`staging` o PR a `main`
+- **Destino**: staging.pythoncdmx.org
+- **S3 Bucket**: pythoncdmx-website-staging
+- **Cache**: Corto (1min-2h)
+- **Banner**: "🚧 STAGING ENVIRONMENT" en todas las páginas
+
+## 📊 Gestión de Cache
+
+### Invalidar Cache Completo
+
+```bash
+aws cloudfront create-invalidation \
+ --distribution-id E1234ABCDEF567 \
+ --paths "/*"
+```
+
+### Invalidar Paths Específicos
+
+```bash
+aws cloudfront create-invalidation \
+ --distribution-id E1234ABCDEF567 \
+ --paths "/index.html" "/css/*"
+```
+
+### Verificar Estado de Invalidación
+
+```bash
+aws cloudfront get-invalidation \
+ --distribution-id E1234ABCDEF567 \
+ --id I1234ABCDEF567
+```
+
+## 🔄 Estrategia de Cache
+
+### HTML Files
+- Cache: 10 minutos
+- Header: `Cache-Control: public, max-age=600, must-revalidate`
+
+### Assets Estáticos (CSS/JS)
+- Cache: 24 horas
+- Header: `Cache-Control: public, max-age=86400`
+
+### Imágenes
+- Cache: 7 días
+- Header: `Cache-Control: public, max-age=604800`
+
+### Sitemap
+- Cache: Sin cache
+- Header: `Cache-Control: public, max-age=0, must-revalidate`
+
+## 💰 Costos Estimados
+
+### Free Tier (Primer Año)
+- S3: 5GB de almacenamiento
+- CloudFront: 50GB de transferencia
+- ACM: Certificados gratuitos
+
+### Después de Free Tier (Estimado Mensual)
+- S3: ~$0.50 (20GB)
+- CloudFront: ~$2-5 (dependiendo del tráfico)
+- **Total**: ~$3-6/mes
+
+## 🔍 Troubleshooting
+
+### Error: Certificate validation timeout
+
+**Problema**: El certificado ACM no se valida automáticamente.
+
+**Solución**:
+1. Verifica que Route53 tenga los registros de validación:
+ ```bash
+ aws route53 list-resource-record-sets --hosted-zone-id ZXXXXX
+ ```
+2. Los registros deben ser tipo CNAME con nombres `_abc123.pythoncdmx.org`
+3. Terraform crea estos automáticamente, pero puede tardar 5-30 minutos
+4. Si persiste el error después de 45 minutos, revisar permisos de Route53
+
+### Error: S3 bucket already exists
+
+**Problema**: El nombre del bucket ya está en uso.
+
+**Solución**:
+```bash
+# Cambiar nombre en variables.tf
+bucket_name = "pythoncdmx-website-prod"
+```
+
+### Error: Access Denied al subir a S3
+
+**Problema**: Permisos insuficientes del IAM role.
+
+**Solución**:
+1. Verificar políticas del role: `terraform/iam-github.tf`
+2. Confirmar trust relationship con GitHub OIDC
+3. Revisar logs de CloudWatch
+
+### CloudFront muestra contenido antiguo
+
+**Problema**: Cache no invalidado.
+
+**Solución**:
+```bash
+aws cloudfront create-invalidation \
+ --distribution-id $DISTRIBUTION_ID \
+ --paths "/*"
+```
+
+## 🔒 Seguridad
+
+### Buenas Prácticas Implementadas
+
+✅ **S3 Bucket privado**: No acceso público directo
+✅ **Origin Access Control**: CloudFront usa firma SigV4
+✅ **Encriptación en reposo**: AES256 en S3
+✅ **TLS 1.2+**: Protocolo mínimo seguro
+✅ **Versioning**: Protección contra eliminación accidental
+✅ **IAM Roles**: Sin credenciales hardcoded
+✅ **OIDC GitHub**: Autenticación sin long-lived tokens
+
+### Checklist de Seguridad
+
+- [ ] Backend state encriptado
+- [ ] S3 bucket policy restrictiva
+- [ ] CloudFront usa HTTPS únicamente
+- [ ] Certificado SSL válido
+- [ ] IAM roles con least privilege
+- [ ] Logs de acceso habilitados (opcional)
+- [ ] AWS WAF configurado (opcional para producción)
+
+## 🛠️ Mantenimiento
+
+### Actualizar Infraestructura
+
+```bash
+cd terraform
+terraform plan
+terraform apply
+```
+
+### Backup de State
+
+```bash
+# Descargar state actual
+aws s3 cp s3://pythoncdmx-terraform-state/website/terraform.tfstate ./backup-$(date +%Y%m%d).tfstate
+```
+
+### Destruir Infraestructura (PELIGRO)
+
+```bash
+# ⚠️ Esto eliminará TODOS los recursos
+terraform destroy
+```
+
+## 🌍 Gestión de Entornos
+
+### Diferencias Production vs Staging
+
+| Aspecto | Production | Staging |
+|---------|-----------|---------|
+| **Dominio** | pythoncdmx.org | staging.pythoncdmx.org |
+| **S3 Bucket** | pythoncdmx-website | pythoncdmx-website-staging |
+| **Cache HTML** | 10 minutos | 1 minuto |
+| **Cache Assets** | 24 horas | 30 minutos |
+| **Cache Images** | 7 días | 1 hora |
+| **Lifecycle S3** | 90 días | 30 días |
+| **Deploy Trigger** | Push a `main` | Push a `develop`/staging |
+| **Banner** | No | Sí ("STAGING ENV") |
+
+### Flujo de Trabajo Recomendado
+
+1. **Desarrollo**: Crear branch de feature
+2. **Testing**: Merge a `develop` → Deploy a staging
+3. **QA**: Probar en https://staging.pythoncdmx.org
+4. **Producción**: PR a `main` → Review → Merge → Deploy automático
+
+## 📚 Referencias
+
+- [Terraform AWS Provider](https://registry.terraform.io/providers/hashicorp/aws/latest/docs)
+- [CloudFront con S3](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/GettingStarted.SimpleDistribution.html)
+- [ACM Certificate Validation](https://docs.aws.amazon.com/acm/latest/userguide/dns-validation.html)
+- [Route53 Records](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-values.html)
+- [GitHub OIDC con AWS](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services)
+
+## 📞 Soporte
+
+Para problemas con la infraestructura:
+1. Revisar esta documentación
+2. Consultar logs de CloudWatch
+3. Verificar estado de recursos: `terraform show`
+4. Abrir issue en el repositorio
+5. Contactar al equipo de infraestructura
+
+---
+
+**Última actualización**: 2025-01-11
+**Mantenido por**: Equipo Python CDMX
+**Versión**: 2.0 (Route53 + Staging Environment)
diff --git a/terraform/acm-staging.tf b/terraform/acm-staging.tf
new file mode 100644
index 0000000..f2bb745
--- /dev/null
+++ b/terraform/acm-staging.tf
@@ -0,0 +1,31 @@
+# ACM certificate for CloudFront staging (must be in us-east-1)
+resource "aws_acm_certificate" "website_staging" {
+ provider = aws.us_east_1
+
+ domain_name = var.staging_subdomain
+ validation_method = "DNS"
+
+ lifecycle {
+ create_before_destroy = true
+ }
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "PythonCDMX Website Certificate - Staging"
+ Environment = "staging"
+ }
+ )
+}
+
+# Certificate validation for staging
+resource "aws_acm_certificate_validation" "website_staging" {
+ provider = aws.us_east_1
+
+ certificate_arn = aws_acm_certificate.website_staging.arn
+ validation_record_fqdns = [for record in aws_route53_record.certificate_validation_staging : record.fqdn]
+
+ timeouts {
+ create = "45m"
+ }
+}
diff --git a/terraform/acm.tf b/terraform/acm.tf
new file mode 100644
index 0000000..84ebc7d
--- /dev/null
+++ b/terraform/acm.tf
@@ -0,0 +1,30 @@
+# ACM certificate for CloudFront (must be in us-east-1)
+resource "aws_acm_certificate" "website" {
+ provider = aws.us_east_1
+
+ domain_name = var.domain_name
+ subject_alternative_names = var.alternative_domain_names
+ validation_method = "DNS"
+
+ lifecycle {
+ create_before_destroy = true
+ }
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "PythonCDMX Website Certificate"
+ }
+ )
+}
+
+# Certificate validation
+resource "aws_acm_certificate_validation" "website" {
+ provider = aws.us_east_1
+
+ certificate_arn = aws_acm_certificate.website.arn
+
+ timeouts {
+ create = "45m"
+ }
+}
diff --git a/terraform/cloudfront-staging.tf b/terraform/cloudfront-staging.tf
new file mode 100644
index 0000000..3504eb0
--- /dev/null
+++ b/terraform/cloudfront-staging.tf
@@ -0,0 +1,144 @@
+# Origin Access Control for S3 Staging
+resource "aws_cloudfront_origin_access_control" "website_staging" {
+ name = "pythoncdmx-oac-staging"
+ description = "OAC for PythonCDMX staging website"
+ origin_access_control_origin_type = "s3"
+ signing_behavior = "always"
+ signing_protocol = "sigv4"
+}
+
+# CloudFront distribution for staging
+resource "aws_cloudfront_distribution" "website_staging" {
+ enabled = true
+ is_ipv6_enabled = true
+ default_root_object = "index.html"
+ price_class = "PriceClass_100" # Use only North America and Europe
+ comment = "PythonCDMX Community Website - Staging"
+
+ aliases = [var.staging_subdomain]
+
+ origin {
+ domain_name = aws_s3_bucket.website_staging.bucket_regional_domain_name
+ origin_id = "S3-${var.staging_bucket_name}"
+ origin_access_control_id = aws_cloudfront_origin_access_control.website_staging.id
+ }
+
+ default_cache_behavior {
+ allowed_methods = ["GET", "HEAD", "OPTIONS"]
+ cached_methods = ["GET", "HEAD"]
+ target_origin_id = "S3-${var.staging_bucket_name}"
+
+ forwarded_values {
+ query_string = false
+ headers = ["Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"]
+
+ cookies {
+ forward = "none"
+ }
+ }
+
+ viewer_protocol_policy = "redirect-to-https"
+ min_ttl = 0
+ default_ttl = 300 # 5 minutes - shorter cache for staging
+ max_ttl = 3600 # 1 hour - shorter cache for staging
+ compress = true
+ }
+
+ # Cache behavior for static assets (CSS) - shorter cache for staging
+ ordered_cache_behavior {
+ path_pattern = "/css/*"
+ allowed_methods = ["GET", "HEAD", "OPTIONS"]
+ cached_methods = ["GET", "HEAD"]
+ target_origin_id = "S3-${var.staging_bucket_name}"
+
+ forwarded_values {
+ query_string = false
+ cookies {
+ forward = "none"
+ }
+ }
+
+ viewer_protocol_policy = "redirect-to-https"
+ min_ttl = 0
+ default_ttl = 1800 # 30 minutes
+ max_ttl = 7200 # 2 hours
+ compress = true
+ }
+
+ # Cache behavior for static assets (JS) - shorter cache for staging
+ ordered_cache_behavior {
+ path_pattern = "/js/*"
+ allowed_methods = ["GET", "HEAD", "OPTIONS"]
+ cached_methods = ["GET", "HEAD"]
+ target_origin_id = "S3-${var.staging_bucket_name}"
+
+ forwarded_values {
+ query_string = false
+ cookies {
+ forward = "none"
+ }
+ }
+
+ viewer_protocol_policy = "redirect-to-https"
+ min_ttl = 0
+ default_ttl = 1800 # 30 minutes
+ max_ttl = 7200 # 2 hours
+ compress = true
+ }
+
+ # Cache behavior for images - shorter cache for staging
+ ordered_cache_behavior {
+ path_pattern = "/images/*"
+ allowed_methods = ["GET", "HEAD", "OPTIONS"]
+ cached_methods = ["GET", "HEAD"]
+ target_origin_id = "S3-${var.staging_bucket_name}"
+
+ forwarded_values {
+ query_string = false
+ cookies {
+ forward = "none"
+ }
+ }
+
+ viewer_protocol_policy = "redirect-to-https"
+ min_ttl = 0
+ default_ttl = 3600 # 1 hour
+ max_ttl = 86400 # 24 hours
+ compress = true
+ }
+
+ # Custom error responses for SPA-like behavior
+ custom_error_response {
+ error_code = 404
+ response_code = 404
+ response_page_path = "/404.html"
+ }
+
+ custom_error_response {
+ error_code = 403
+ response_code = 404
+ response_page_path = "/404.html"
+ }
+
+ restrictions {
+ geo_restriction {
+ restriction_type = "none"
+ }
+ }
+
+ viewer_certificate {
+ acm_certificate_arn = aws_acm_certificate.website_staging.arn
+ ssl_support_method = "sni-only"
+ minimum_protocol_version = "TLSv1.2_2021"
+ }
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "PythonCDMX Website Distribution - Staging"
+ Environment = "staging"
+ }
+ )
+
+ depends_on = [aws_acm_certificate_validation.website_staging]
+}
diff --git a/terraform/cloudfront.tf b/terraform/cloudfront.tf
new file mode 100644
index 0000000..8e7da3c
--- /dev/null
+++ b/terraform/cloudfront.tf
@@ -0,0 +1,141 @@
+# Origin Access Control for S3
+resource "aws_cloudfront_origin_access_control" "website" {
+ name = "pythoncdmx-oac"
+ description = "OAC for PythonCDMX website"
+ origin_access_control_origin_type = "s3"
+ signing_behavior = "always"
+ signing_protocol = "sigv4"
+}
+
+# CloudFront distribution
+resource "aws_cloudfront_distribution" "website" {
+ enabled = true
+ is_ipv6_enabled = true
+ default_root_object = "index.html"
+ price_class = "PriceClass_100" # Use only North America and Europe
+ comment = "PythonCDMX Community Website"
+
+ aliases = concat([var.domain_name], var.alternative_domain_names)
+
+ origin {
+ domain_name = aws_s3_bucket.website.bucket_regional_domain_name
+ origin_id = "S3-${var.bucket_name}"
+ origin_access_control_id = aws_cloudfront_origin_access_control.website.id
+ }
+
+ default_cache_behavior {
+ allowed_methods = ["GET", "HEAD", "OPTIONS"]
+ cached_methods = ["GET", "HEAD"]
+ target_origin_id = "S3-${var.bucket_name}"
+
+ forwarded_values {
+ query_string = false
+ headers = ["Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"]
+
+ cookies {
+ forward = "none"
+ }
+ }
+
+ viewer_protocol_policy = "redirect-to-https"
+ min_ttl = 0
+ default_ttl = 3600 # 1 hour
+ max_ttl = 86400 # 24 hours
+ compress = true
+ }
+
+ # Cache behavior for static assets (images, CSS, JS)
+ ordered_cache_behavior {
+ path_pattern = "/css/*"
+ allowed_methods = ["GET", "HEAD", "OPTIONS"]
+ cached_methods = ["GET", "HEAD"]
+ target_origin_id = "S3-${var.bucket_name}"
+
+ forwarded_values {
+ query_string = false
+ cookies {
+ forward = "none"
+ }
+ }
+
+ viewer_protocol_policy = "redirect-to-https"
+ min_ttl = 0
+ default_ttl = 86400 # 24 hours
+ max_ttl = 31536000 # 1 year
+ compress = true
+ }
+
+ ordered_cache_behavior {
+ path_pattern = "/js/*"
+ allowed_methods = ["GET", "HEAD", "OPTIONS"]
+ cached_methods = ["GET", "HEAD"]
+ target_origin_id = "S3-${var.bucket_name}"
+
+ forwarded_values {
+ query_string = false
+ cookies {
+ forward = "none"
+ }
+ }
+
+ viewer_protocol_policy = "redirect-to-https"
+ min_ttl = 0
+ default_ttl = 86400 # 24 hours
+ max_ttl = 31536000 # 1 year
+ compress = true
+ }
+
+ ordered_cache_behavior {
+ path_pattern = "/images/*"
+ allowed_methods = ["GET", "HEAD", "OPTIONS"]
+ cached_methods = ["GET", "HEAD"]
+ target_origin_id = "S3-${var.bucket_name}"
+
+ forwarded_values {
+ query_string = false
+ cookies {
+ forward = "none"
+ }
+ }
+
+ viewer_protocol_policy = "redirect-to-https"
+ min_ttl = 0
+ default_ttl = 604800 # 7 days
+ max_ttl = 31536000 # 1 year
+ compress = true
+ }
+
+ # Custom error responses for SPA-like behavior
+ custom_error_response {
+ error_code = 404
+ response_code = 404
+ response_page_path = "/404.html"
+ }
+
+ custom_error_response {
+ error_code = 403
+ response_code = 404
+ response_page_path = "/404.html"
+ }
+
+ restrictions {
+ geo_restriction {
+ restriction_type = "none"
+ }
+ }
+
+ viewer_certificate {
+ acm_certificate_arn = aws_acm_certificate.website.arn
+ ssl_support_method = "sni-only"
+ minimum_protocol_version = "TLSv1.2_2021"
+ }
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "PythonCDMX Website Distribution"
+ }
+ )
+
+ depends_on = [aws_acm_certificate_validation.website]
+}
diff --git a/terraform/iam-github.tf b/terraform/iam-github.tf
new file mode 100644
index 0000000..70ce23f
--- /dev/null
+++ b/terraform/iam-github.tf
@@ -0,0 +1,136 @@
+# IAM role for GitHub Actions OIDC
+# This allows GitHub Actions to authenticate with AWS without long-lived credentials
+
+data "aws_caller_identity" "current" {}
+
+# OIDC Provider for GitHub Actions
+resource "aws_iam_openid_connect_provider" "github" {
+ url = "https://token.actions.githubusercontent.com"
+
+ client_id_list = [
+ "sts.amazonaws.com"
+ ]
+
+ thumbprint_list = [
+ "6938fd4d98bab03faadb97b34396831e3780aea1",
+ "1c58a3a8518e8759bf075b76b750d4f2df264fcd"
+ ]
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "GitHub Actions OIDC Provider"
+ }
+ )
+}
+
+# IAM Role for GitHub Actions
+resource "aws_iam_role" "github_actions" {
+ name = "GitHubActionsDeployRole"
+ description = "Role for GitHub Actions to deploy to S3 and invalidate CloudFront"
+ assume_role_policy = data.aws_iam_policy_document.github_actions_assume_role.json
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "GitHub Actions Deploy Role"
+ }
+ )
+}
+
+# Trust policy for GitHub OIDC
+data "aws_iam_policy_document" "github_actions_assume_role" {
+ statement {
+ effect = "Allow"
+ actions = ["sts:AssumeRoleWithWebIdentity"]
+
+ principals {
+ type = "Federated"
+ identifiers = [aws_iam_openid_connect_provider.github.arn]
+ }
+
+ condition {
+ test = "StringEquals"
+ variable = "token.actions.githubusercontent.com:aud"
+ values = ["sts.amazonaws.com"]
+ }
+
+ condition {
+ test = "StringLike"
+ variable = "token.actions.githubusercontent.com:sub"
+ values = ["repo:PythonMexico/pythonCDMX:*"]
+ }
+ }
+}
+
+# Policy for S3 access (production and staging)
+data "aws_iam_policy_document" "github_actions_s3" {
+ statement {
+ sid = "AllowS3AccessProduction"
+ effect = "Allow"
+
+ actions = [
+ "s3:PutObject",
+ "s3:PutObjectAcl",
+ "s3:GetObject",
+ "s3:DeleteObject",
+ "s3:ListBucket"
+ ]
+
+ resources = [
+ aws_s3_bucket.website.arn,
+ "${aws_s3_bucket.website.arn}/*"
+ ]
+ }
+
+ statement {
+ sid = "AllowS3AccessStaging"
+ effect = "Allow"
+
+ actions = [
+ "s3:PutObject",
+ "s3:PutObjectAcl",
+ "s3:GetObject",
+ "s3:DeleteObject",
+ "s3:ListBucket"
+ ]
+
+ resources = [
+ aws_s3_bucket.website_staging.arn,
+ "${aws_s3_bucket.website_staging.arn}/*"
+ ]
+ }
+}
+
+# Policy for CloudFront invalidation (production and staging)
+data "aws_iam_policy_document" "github_actions_cloudfront" {
+ statement {
+ sid = "AllowCloudFrontInvalidation"
+ effect = "Allow"
+
+ actions = [
+ "cloudfront:CreateInvalidation",
+ "cloudfront:GetInvalidation",
+ "cloudfront:ListInvalidations"
+ ]
+
+ resources = [
+ aws_cloudfront_distribution.website.arn,
+ aws_cloudfront_distribution.website_staging.arn
+ ]
+ }
+}
+
+# Attach S3 policy to role
+resource "aws_iam_role_policy" "github_actions_s3" {
+ name = "S3AccessPolicy"
+ role = aws_iam_role.github_actions.id
+ policy = data.aws_iam_policy_document.github_actions_s3.json
+}
+
+# Attach CloudFront policy to role
+resource "aws_iam_role_policy" "github_actions_cloudfront" {
+ name = "CloudFrontAccessPolicy"
+ role = aws_iam_role.github_actions.id
+ policy = data.aws_iam_policy_document.github_actions_cloudfront.json
+}
diff --git a/terraform/main.tf b/terraform/main.tf
new file mode 100644
index 0000000..e0879fe
--- /dev/null
+++ b/terraform/main.tf
@@ -0,0 +1,43 @@
+terraform {
+ required_version = ">= 1.0"
+
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = "~> 5.0"
+ }
+ }
+
+ backend "s3" {
+ bucket = "bucket-terraform-a8ab"
+ key = "pythoncdmx/terraform.tfstate"
+ region = "us-east-1"
+ encrypt = true
+ }
+}
+
+provider "aws" {
+ region = var.aws_region
+
+ default_tags {
+ tags = {
+ Project = "PythonCDMX"
+ Environment = var.environment
+ ManagedBy = "Terraform"
+ }
+ }
+}
+
+# Provider for ACM certificates (must be in us-east-1 for CloudFront)
+provider "aws" {
+ alias = "us_east_1"
+ region = "us-east-1"
+
+ default_tags {
+ tags = {
+ Project = "PythonCDMX"
+ Environment = var.environment
+ ManagedBy = "Terraform"
+ }
+ }
+}
diff --git a/terraform/outputs.tf b/terraform/outputs.tf
new file mode 100644
index 0000000..18001f7
--- /dev/null
+++ b/terraform/outputs.tf
@@ -0,0 +1,88 @@
+output "website_bucket_name" {
+ description = "Name of the S3 bucket hosting the website"
+ value = aws_s3_bucket.website.id
+}
+
+output "website_bucket_arn" {
+ description = "ARN of the S3 bucket hosting the website"
+ value = aws_s3_bucket.website.arn
+}
+
+output "cloudfront_distribution_id" {
+ description = "ID of the CloudFront distribution"
+ value = aws_cloudfront_distribution.website.id
+}
+
+output "cloudfront_domain_name" {
+ description = "Domain name of the CloudFront distribution"
+ value = aws_cloudfront_distribution.website.domain_name
+}
+
+output "certificate_arn" {
+ description = "ARN of the ACM certificate"
+ value = aws_acm_certificate.website.arn
+}
+
+output "certificate_validation_records" {
+ description = "DNS validation records for the certificate"
+ value = [
+ for dvo in aws_acm_certificate.website.domain_validation_options : {
+ name = dvo.resource_record_name
+ type = dvo.resource_record_type
+ value = dvo.resource_record_value
+ }
+ ]
+}
+
+output "website_url" {
+ description = "URL of the website (production)"
+ value = "https://${var.domain_name}"
+}
+
+# ============================================================================
+# STAGING ENVIRONMENT OUTPUTS
+# ============================================================================
+
+output "staging_bucket_name" {
+ description = "Name of the S3 bucket hosting the staging website"
+ value = aws_s3_bucket.website_staging.id
+}
+
+output "staging_bucket_arn" {
+ description = "ARN of the S3 bucket hosting the staging website"
+ value = aws_s3_bucket.website_staging.arn
+}
+
+output "staging_cloudfront_distribution_id" {
+ description = "ID of the CloudFront distribution (staging)"
+ value = aws_cloudfront_distribution.website_staging.id
+}
+
+output "staging_cloudfront_domain_name" {
+ description = "Domain name of the CloudFront distribution (staging)"
+ value = aws_cloudfront_distribution.website_staging.domain_name
+}
+
+output "staging_certificate_arn" {
+ description = "ARN of the ACM certificate (staging)"
+ value = aws_acm_certificate.website_staging.arn
+}
+
+output "staging_website_url" {
+ description = "URL of the staging website"
+ value = "https://${var.staging_subdomain}"
+}
+
+# ============================================================================
+# ROUTE53 OUTPUTS
+# ============================================================================
+
+output "hosted_zone_id" {
+ description = "ID of the Route53 hosted zone"
+ value = data.aws_route53_zone.main.zone_id
+}
+
+output "hosted_zone_name_servers" {
+ description = "Name servers for the hosted zone"
+ value = data.aws_route53_zone.main.name_servers
+}
diff --git a/terraform/required-permissions.json b/terraform/required-permissions.json
new file mode 100644
index 0000000..06a9d53
--- /dev/null
+++ b/terraform/required-permissions.json
@@ -0,0 +1,113 @@
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "ACMCertificateManagement",
+ "Effect": "Allow",
+ "Action": [
+ "acm:RequestCertificate",
+ "acm:DescribeCertificate",
+ "acm:ListCertificates",
+ "acm:DeleteCertificate",
+ "acm:AddTagsToCertificate",
+ "acm:ListTagsForCertificate"
+ ],
+ "Resource": "*"
+ },
+ {
+ "Sid": "CloudFrontManagement",
+ "Effect": "Allow",
+ "Action": [
+ "cloudfront:CreateDistribution",
+ "cloudfront:GetDistribution",
+ "cloudfront:GetDistributionConfig",
+ "cloudfront:UpdateDistribution",
+ "cloudfront:DeleteDistribution",
+ "cloudfront:TagResource",
+ "cloudfront:CreateOriginAccessControl",
+ "cloudfront:GetOriginAccessControl",
+ "cloudfront:UpdateOriginAccessControl",
+ "cloudfront:DeleteOriginAccessControl",
+ "cloudfront:CreateInvalidation",
+ "cloudfront:GetInvalidation",
+ "cloudfront:ListInvalidations"
+ ],
+ "Resource": "*"
+ },
+ {
+ "Sid": "Route53DNSManagement",
+ "Effect": "Allow",
+ "Action": [
+ "route53:GetHostedZone",
+ "route53:ListHostedZones",
+ "route53:ListResourceRecordSets",
+ "route53:ChangeResourceRecordSets",
+ "route53:GetChange",
+ "route53:ListTagsForResource"
+ ],
+ "Resource": "*"
+ },
+ {
+ "Sid": "S3BucketManagement",
+ "Effect": "Allow",
+ "Action": [
+ "s3:CreateBucket",
+ "s3:DeleteBucket",
+ "s3:ListBucket",
+ "s3:GetBucketLocation",
+ "s3:GetBucketPolicy",
+ "s3:PutBucketPolicy",
+ "s3:DeleteBucketPolicy",
+ "s3:GetBucketVersioning",
+ "s3:PutBucketVersioning",
+ "s3:GetBucketPublicAccessBlock",
+ "s3:PutBucketPublicAccessBlock",
+ "s3:GetBucketCORS",
+ "s3:PutBucketCORS",
+ "s3:GetEncryptionConfiguration",
+ "s3:PutEncryptionConfiguration",
+ "s3:GetLifecycleConfiguration",
+ "s3:PutLifecycleConfiguration",
+ "s3:GetBucketTagging",
+ "s3:PutBucketTagging",
+ "s3:GetObject",
+ "s3:PutObject",
+ "s3:DeleteObject",
+ "s3:PutObjectAcl"
+ ],
+ "Resource": [
+ "arn:aws:s3:::pythoncdmx-website",
+ "arn:aws:s3:::pythoncdmx-website/*",
+ "arn:aws:s3:::pythoncdmx-website-staging",
+ "arn:aws:s3:::pythoncdmx-website-staging/*"
+ ]
+ },
+ {
+ "Sid": "IAMRoleManagement",
+ "Effect": "Allow",
+ "Action": [
+ "iam:CreateOpenIDConnectProvider",
+ "iam:GetOpenIDConnectProvider",
+ "iam:DeleteOpenIDConnectProvider",
+ "iam:TagOpenIDConnectProvider",
+ "iam:CreateRole",
+ "iam:GetRole",
+ "iam:DeleteRole",
+ "iam:UpdateAssumeRolePolicy",
+ "iam:AttachRolePolicy",
+ "iam:DetachRolePolicy",
+ "iam:PutRolePolicy",
+ "iam:GetRolePolicy",
+ "iam:DeleteRolePolicy",
+ "iam:ListRolePolicies",
+ "iam:ListAttachedRolePolicies",
+ "iam:TagRole",
+ "iam:ListRoleTags"
+ ],
+ "Resource": [
+ "arn:aws:iam::700463753979:oidc-provider/token.actions.githubusercontent.com",
+ "arn:aws:iam::700463753979:role/GitHubActionsDeployRole"
+ ]
+ }
+ ]
+}
diff --git a/terraform/route53.tf b/terraform/route53.tf
new file mode 100644
index 0000000..ad6f463
--- /dev/null
+++ b/terraform/route53.tf
@@ -0,0 +1,131 @@
+# Data source for existing hosted zone
+data "aws_route53_zone" "main" {
+ name = var.domain_name
+ private_zone = false
+}
+
+# ============================================================================
+# PRODUCTION ENVIRONMENT - pythoncdmx.org
+# ============================================================================
+
+# A record for root domain (pythoncdmx.org) pointing to CloudFront
+resource "aws_route53_record" "website_root" {
+ zone_id = data.aws_route53_zone.main.zone_id
+ name = var.domain_name
+ type = "A"
+
+ alias {
+ name = aws_cloudfront_distribution.website.domain_name
+ zone_id = aws_cloudfront_distribution.website.hosted_zone_id
+ evaluate_target_health = false
+ }
+}
+
+# AAAA record for IPv6 support (root domain)
+resource "aws_route53_record" "website_root_ipv6" {
+ zone_id = data.aws_route53_zone.main.zone_id
+ name = var.domain_name
+ type = "AAAA"
+
+ alias {
+ name = aws_cloudfront_distribution.website.domain_name
+ zone_id = aws_cloudfront_distribution.website.hosted_zone_id
+ evaluate_target_health = false
+ }
+}
+
+# A record for www subdomain pointing to CloudFront
+resource "aws_route53_record" "website_www" {
+ zone_id = data.aws_route53_zone.main.zone_id
+ name = "www.${var.domain_name}"
+ type = "A"
+
+ alias {
+ name = aws_cloudfront_distribution.website.domain_name
+ zone_id = aws_cloudfront_distribution.website.hosted_zone_id
+ evaluate_target_health = false
+ }
+}
+
+# AAAA record for IPv6 support (www subdomain)
+resource "aws_route53_record" "website_www_ipv6" {
+ zone_id = data.aws_route53_zone.main.zone_id
+ name = "www.${var.domain_name}"
+ type = "AAAA"
+
+ alias {
+ name = aws_cloudfront_distribution.website.domain_name
+ zone_id = aws_cloudfront_distribution.website.hosted_zone_id
+ evaluate_target_health = false
+ }
+}
+
+# ============================================================================
+# STAGING ENVIRONMENT - staging.pythoncdmx.org
+# ============================================================================
+
+# A record for staging subdomain pointing to CloudFront staging distribution
+resource "aws_route53_record" "website_staging" {
+ zone_id = data.aws_route53_zone.main.zone_id
+ name = var.staging_subdomain
+ type = "A"
+
+ alias {
+ name = aws_cloudfront_distribution.website_staging.domain_name
+ zone_id = aws_cloudfront_distribution.website_staging.hosted_zone_id
+ evaluate_target_health = false
+ }
+}
+
+# AAAA record for IPv6 support (staging subdomain)
+resource "aws_route53_record" "website_staging_ipv6" {
+ zone_id = data.aws_route53_zone.main.zone_id
+ name = var.staging_subdomain
+ type = "AAAA"
+
+ alias {
+ name = aws_cloudfront_distribution.website_staging.domain_name
+ zone_id = aws_cloudfront_distribution.website_staging.hosted_zone_id
+ evaluate_target_health = false
+ }
+}
+
+# ============================================================================
+# ACM CERTIFICATE VALIDATION RECORDS
+# ============================================================================
+
+# Validation records for production certificate
+resource "aws_route53_record" "certificate_validation" {
+ for_each = {
+ for dvo in aws_acm_certificate.website.domain_validation_options : dvo.domain_name => {
+ name = dvo.resource_record_name
+ record = dvo.resource_record_value
+ type = dvo.resource_record_type
+ }
+ }
+
+ allow_overwrite = true
+ name = each.value.name
+ records = [each.value.record]
+ ttl = 60
+ type = each.value.type
+ zone_id = data.aws_route53_zone.main.zone_id
+}
+
+# Validation records for staging certificate
+resource "aws_route53_record" "certificate_validation_staging" {
+ for_each = {
+ for dvo in aws_acm_certificate.website_staging.domain_validation_options : dvo.domain_name => {
+ name = dvo.resource_record_name
+ record = dvo.resource_record_value
+ type = dvo.resource_record_type
+ }
+ }
+
+ allow_overwrite = true
+ name = each.value.name
+ records = [each.value.record]
+ ttl = 60
+ type = each.value.type
+ zone_id = data.aws_route53_zone.main.zone_id
+}
diff --git a/terraform/s3-staging.tf b/terraform/s3-staging.tf
new file mode 100644
index 0000000..731313d
--- /dev/null
+++ b/terraform/s3-staging.tf
@@ -0,0 +1,111 @@
+# S3 bucket for staging website content
+resource "aws_s3_bucket" "website_staging" {
+ bucket = var.staging_bucket_name
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "PythonCDMX Website Staging"
+ Environment = "staging"
+ }
+ )
+}
+
+# Enable versioning for staging bucket
+resource "aws_s3_bucket_versioning" "website_staging" {
+ bucket = aws_s3_bucket.website_staging.id
+
+ versioning_configuration {
+ status = "Enabled"
+ }
+}
+
+# Block public access at bucket level (CloudFront OAC will handle access)
+resource "aws_s3_bucket_public_access_block" "website_staging" {
+ bucket = aws_s3_bucket.website_staging.id
+
+ block_public_acls = true
+ block_public_policy = true
+ ignore_public_acls = true
+ restrict_public_buckets = true
+}
+
+# Bucket policy to allow CloudFront access
+resource "aws_s3_bucket_policy" "website_staging" {
+ bucket = aws_s3_bucket.website_staging.id
+
+ policy = jsonencode({
+ Version = "2012-10-17"
+ Statement = [
+ {
+ Sid = "AllowCloudFrontServicePrincipal"
+ Effect = "Allow"
+ Principal = {
+ Service = "cloudfront.amazonaws.com"
+ }
+ Action = "s3:GetObject"
+ Resource = "${aws_s3_bucket.website_staging.arn}/*"
+ Condition = {
+ StringEquals = {
+ "AWS:SourceArn" = aws_cloudfront_distribution.website_staging.arn
+ }
+ }
+ }
+ ]
+ })
+
+ depends_on = [aws_cloudfront_distribution.website_staging]
+}
+
+# Enable server-side encryption
+resource "aws_s3_bucket_server_side_encryption_configuration" "website_staging" {
+ bucket = aws_s3_bucket.website_staging.id
+
+ rule {
+ apply_server_side_encryption_by_default {
+ sse_algorithm = "AES256"
+ }
+ }
+}
+
+# Configure lifecycle rules - more aggressive cleanup for staging
+resource "aws_s3_bucket_lifecycle_configuration" "website_staging" {
+ bucket = aws_s3_bucket.website_staging.id
+
+ rule {
+ id = "delete-old-versions"
+ status = "Enabled"
+
+ filter {}
+
+ noncurrent_version_expiration {
+ noncurrent_days = 30 # Shorter retention for staging
+ }
+ }
+
+ rule {
+ id = "delete-incomplete-uploads"
+ status = "Enabled"
+
+ filter {}
+
+ abort_incomplete_multipart_upload {
+ days_after_initiation = 3
+ }
+ }
+}
+
+# CORS configuration for website assets
+resource "aws_s3_bucket_cors_configuration" "website_staging" {
+ bucket = aws_s3_bucket.website_staging.id
+
+ cors_rule {
+ allowed_headers = ["*"]
+ allowed_methods = ["GET", "HEAD"]
+ allowed_origins = [
+ "https://${var.staging_subdomain}"
+ ]
+ expose_headers = ["ETag"]
+ max_age_seconds = 3600
+ }
+}
diff --git a/terraform/s3.tf b/terraform/s3.tf
new file mode 100644
index 0000000..241313a
--- /dev/null
+++ b/terraform/s3.tf
@@ -0,0 +1,111 @@
+# S3 bucket for website content
+resource "aws_s3_bucket" "website" {
+ bucket = var.bucket_name
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "PythonCDMX Website"
+ }
+ )
+}
+
+# Enable versioning for backup purposes
+resource "aws_s3_bucket_versioning" "website" {
+ bucket = aws_s3_bucket.website.id
+
+ versioning_configuration {
+ status = "Enabled"
+ }
+}
+
+# Block public access at bucket level (CloudFront OAC will handle access)
+resource "aws_s3_bucket_public_access_block" "website" {
+ bucket = aws_s3_bucket.website.id
+
+ block_public_acls = true
+ block_public_policy = true
+ ignore_public_acls = true
+ restrict_public_buckets = true
+}
+
+# Bucket policy to allow CloudFront access
+resource "aws_s3_bucket_policy" "website" {
+ bucket = aws_s3_bucket.website.id
+
+ policy = jsonencode({
+ Version = "2012-10-17"
+ Statement = [
+ {
+ Sid = "AllowCloudFrontServicePrincipal"
+ Effect = "Allow"
+ Principal = {
+ Service = "cloudfront.amazonaws.com"
+ }
+ Action = "s3:GetObject"
+ Resource = "${aws_s3_bucket.website.arn}/*"
+ Condition = {
+ StringEquals = {
+ "AWS:SourceArn" = aws_cloudfront_distribution.website.arn
+ }
+ }
+ }
+ ]
+ })
+
+ depends_on = [aws_cloudfront_distribution.website]
+}
+
+# Enable server-side encryption
+resource "aws_s3_bucket_server_side_encryption_configuration" "website" {
+ bucket = aws_s3_bucket.website.id
+
+ rule {
+ apply_server_side_encryption_by_default {
+ sse_algorithm = "AES256"
+ }
+ }
+}
+
+# Configure lifecycle rules
+resource "aws_s3_bucket_lifecycle_configuration" "website" {
+ bucket = aws_s3_bucket.website.id
+
+ rule {
+ id = "delete-old-versions"
+ status = "Enabled"
+
+ filter {}
+
+ noncurrent_version_expiration {
+ noncurrent_days = 90
+ }
+ }
+
+ rule {
+ id = "delete-incomplete-uploads"
+ status = "Enabled"
+
+ filter {}
+
+ abort_incomplete_multipart_upload {
+ days_after_initiation = 7
+ }
+ }
+}
+
+# CORS configuration for website assets
+resource "aws_s3_bucket_cors_configuration" "website" {
+ bucket = aws_s3_bucket.website.id
+
+ cors_rule {
+ allowed_headers = ["*"]
+ allowed_methods = ["GET", "HEAD"]
+ allowed_origins = [
+ "https://${var.domain_name}",
+ "https://www.${var.domain_name}"
+ ]
+ expose_headers = ["ETag"]
+ max_age_seconds = 3600
+ }
+}
diff --git a/terraform/terraform.tfvars copy.example b/terraform/terraform.tfvars copy.example
new file mode 100644
index 0000000..14e4bf9
--- /dev/null
+++ b/terraform/terraform.tfvars copy.example
@@ -0,0 +1,27 @@
+# Example Terraform variables file
+# Copy this file to terraform.tfvars and customize as needed
+
+# AWS Region
+aws_region = "us-east-1"
+
+# Environment
+environment = "production"
+
+# Domain configuration
+domain_name = "pythoncdmx.org"
+alternative_domain_names = [
+ "www.pythoncdmx.org"
+]
+staging_subdomain = "staging.pythoncdmx.org"
+
+# S3 bucket names
+bucket_name = "pythoncdmx-website"
+staging_bucket_name = "pythoncdmx-website-staging"
+terraform_state_bucket = "pythoncdmx-terraform-state"
+terraform_locks_table = "pythoncdmx-terraform-locks"
+
+# Additional tags
+tags = {
+ Team = "Infrastructure"
+ Contact = "infra@pythoncdmx.org"
+}
diff --git a/terraform/variables.tf b/terraform/variables.tf
new file mode 100644
index 0000000..81c997a
--- /dev/null
+++ b/terraform/variables.tf
@@ -0,0 +1,59 @@
+variable "aws_region" {
+ description = "AWS region for the infrastructure"
+ type = string
+ default = "us-east-1"
+}
+
+variable "environment" {
+ description = "Environment name (production, staging)"
+ type = string
+ default = "production"
+}
+
+variable "domain_name" {
+ description = "Primary domain name for the website"
+ type = string
+ default = "pythoncdmx.org"
+}
+
+variable "alternative_domain_names" {
+ description = "Alternative domain names (e.g., www subdomain)"
+ type = list(string)
+ default = ["www.pythoncdmx.org"]
+}
+
+variable "staging_subdomain" {
+ description = "Subdomain for staging environment"
+ type = string
+ default = "staging.pythoncdmx.org"
+}
+
+variable "bucket_name" {
+ description = "S3 bucket name for website hosting (production)"
+ type = string
+ default = "pythoncdmx-website"
+}
+
+variable "staging_bucket_name" {
+ description = "S3 bucket name for staging website"
+ type = string
+ default = "pythoncdmx-website-staging"
+}
+
+variable "terraform_state_bucket" {
+ description = "S3 bucket name for Terraform state"
+ type = string
+ default = "pythoncdmx-terraform-state"
+}
+
+variable "terraform_locks_table" {
+ description = "DynamoDB table for Terraform state locks"
+ type = string
+ default = "pythoncdmx-terraform-locks"
+}
+
+variable "tags" {
+ description = "Additional tags for resources"
+ type = map(string)
+ default = {}
+}