diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000..70e4e0488b
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,22 @@
+# EditorConfig helps maintain consistent coding styles
+# https://editorconfig.org
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 2
+
+[*.md]
+# Trailing whitespace is significant in Markdown (line breaks)
+trim_trailing_whitespace = false
+
+[*.{yaml,yml}]
+indent_size = 2
+
+[Makefile]
+indent_style = tab
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000000..c197ed6e58
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,43 @@
+## Summary
+
+
+
+## Changes
+
+
+
+## Languages affected
+
+
+- [ ] English
+- [ ] Brazilian Portuguese (`content/brazilian-portuguese/`)
+- [ ] Spanish (`content/espanol/`)
+- [ ] Korean (`content/korean/`)
+- [ ] Other: ___
+
+## Checklist
+
+
+
+### Required
+- [ ] Hugo builds without errors (`hugo --quiet`)
+- [ ] All image references use **lowercase** extensions (`.png`, `.jpg`, `.gif` â not `.PNG`)
+- [ ] All `
` tags have quoted attributes and `alt` text
+- [ ] YAML frontmatter has no duplicate keys
+- [ ] If I changed English content, I checked for matching translations and either fixed them too or noted which ones need updating
+
+### If adding images
+- [ ] Images are under 500 KB (compress large screenshots)
+- [ ] Image filenames are descriptive (not `image1.png`)
+- [ ] Images have `width` attributes to control sizing
+
+### If adding a new workshop
+- [ ] Workshop follows the standard folder structure (see CONTRIBUTING.md)
+- [ ] `_index.md` has correct frontmatter (title, description, prereq, difficulty, icon, draft)
+- [ ] Table of contents uses `/` pattern
+- [ ] All activities are numbered with correct `weight` values
+
+### If translating
+- [ ] Translated ALL user-visible text (titles, descriptions, button labels)
+- [ ] Kept image paths, code blocks, and shortcodes unchanged
+- [ ] YAML frontmatter keys are in English (only values are translated)
diff --git a/.github/workflows/content-quality.yaml b/.github/workflows/content-quality.yaml
new file mode 100644
index 0000000000..83bca0bc5b
--- /dev/null
+++ b/.github/workflows/content-quality.yaml
@@ -0,0 +1,192 @@
+name: Content Quality Checks
+
+on:
+ pull_request:
+ paths:
+ - 'content/**'
+ - 'layouts/**'
+ - 'static/**'
+ - 'themes/**'
+ - 'config.toml'
+
+jobs:
+ hugo-build:
+ name: Hugo Build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Setup Hugo
+ uses: peaceiris/actions-hugo@v3
+ with:
+ hugo-version: 'latest'
+ extended: true
+
+ - name: Build site
+ run: hugo --quiet --minify
+
+ content-lint:
+ name: Content Lint
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Get changed files
+ id: changed
+ uses: tj-actions/changed-files@v44
+ with:
+ files: |
+ content/**/*.md
+
+ - name: Check uppercase image extensions
+ if: steps.changed.outputs.any_changed == 'true'
+ run: |
+ echo "đ Checking for uppercase image extensions (.PNG, .JPG, .GIF)..."
+ ERRORS=0
+ for file in ${{ steps.changed.outputs.all_changed_files }}; do
+ if [ -f "$file" ]; then
+ MATCHES=$(grep -n '\.\(PNG\|JPG\|GIF\|JPEG\|BMP\)' "$file" || true)
+ if [ -n "$MATCHES" ]; then
+ echo "â $file:"
+ echo "$MATCHES"
+ ERRORS=$((ERRORS + 1))
+ fi
+ fi
+ done
+ if [ $ERRORS -gt 0 ]; then
+ echo ""
+ echo "â ī¸ Found $ERRORS file(s) with uppercase image extensions."
+ echo " Linux servers are case-sensitive â use .png, .jpg, .gif instead."
+ exit 1
+ fi
+ echo "â
No uppercase image extensions found."
+
+ - name: Check unquoted HTML attributes
+ if: steps.changed.outputs.any_changed == 'true'
+ run: |
+ echo "đ Checking for unquoted HTML attributes..."
+ ERRORS=0
+ for file in ${{ steps.changed.outputs.all_changed_files }}; do
+ if [ -f "$file" ]; then
+ # Match src= or href= followed by a space then non-quote char (unquoted attr)
+ MATCHES=$(grep -n 'src= [^"]' "$file" || true)
+ if [ -n "$MATCHES" ]; then
+ echo "â $file:"
+ echo "$MATCHES"
+ ERRORS=$((ERRORS + 1))
+ fi
+ fi
+ done
+ if [ $ERRORS -gt 0 ]; then
+ echo ""
+ echo "â ī¸ Found $ERRORS file(s) with unquoted HTML attributes."
+ echo " Always quote attributes: src=\"file.png\" not src= file.png"
+ exit 1
+ fi
+ echo "â
No unquoted HTML attributes found."
+
+ - name: Check duplicate YAML frontmatter keys
+ if: steps.changed.outputs.any_changed == 'true'
+ run: |
+ echo "đ Checking for duplicate YAML frontmatter keys..."
+ ERRORS=0
+ for file in ${{ steps.changed.outputs.all_changed_files }}; do
+ if [ -f "$file" ]; then
+ # Extract frontmatter (between --- markers), get keys, find duplicates
+ DUPES=$(sed -n '/^---$/,/^---$/p' "$file" | grep -E '^\w+:' | sed 's/:.*//' | sort | uniq -d)
+ if [ -n "$DUPES" ]; then
+ echo "â $file has duplicate YAML keys:"
+ echo "$DUPES"
+ ERRORS=$((ERRORS + 1))
+ fi
+ fi
+ done
+ if [ $ERRORS -gt 0 ]; then
+ echo ""
+ echo "â ī¸ Found $ERRORS file(s) with duplicate YAML frontmatter keys."
+ echo " Hugo silently uses the last value â remove duplicates."
+ exit 1
+ fi
+ echo "â
No duplicate YAML keys found."
+
+ - name: Check images have alt text
+ if: steps.changed.outputs.any_changed == 'true'
+ run: |
+ echo "đ Checking for images without alt text..."
+ WARNINGS=0
+ for file in ${{ steps.changed.outputs.all_changed_files }}; do
+ if [ -f "$file" ]; then
+ # Check for 
+ MATCHES=$(grep -n '!\[\](' "$file" || true)
+ if [ -n "$MATCHES" ]; then
+ echo "â ī¸ $file has images without alt text:"
+ echo "$MATCHES"
+ WARNINGS=$((WARNINGS + 1))
+ fi
+ # Check for
tags without alt attribute
+ IMG_NO_ALT=$(grep -n '
tags without alt attribute:"
+ echo "$IMG_NO_ALT"
+ WARNINGS=$((WARNINGS + 1))
+ fi
+ fi
+ done
+ if [ $WARNINGS -gt 0 ]; then
+ echo ""
+ echo "â ī¸ Found $WARNINGS file(s) with missing alt text."
+ echo " All images should have descriptive alt text for accessibility."
+ echo " Example: "
+ # Warning only â don't fail the build for alt text (too many existing files)
+ fi
+ echo "â
Alt text check complete."
+
+ translation-parity:
+ name: Translation Parity Check
+ runs-on: ubuntu-latest
+ if: github.event_name == 'pull_request'
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Get changed files
+ id: changed
+ uses: tj-actions/changed-files@v44
+ with:
+ files: |
+ content/**/*.md
+
+ - name: Check if translations exist for changed workshops
+ if: steps.changed.outputs.any_changed == 'true'
+ run: |
+ echo "đ Checking translation parity for changed workshops..."
+ LANGUAGES=("brazilian-portuguese" "espanol" "korean" "francais" "german" "simplified-chinese" "traditional-chinese" "kyrgyz")
+ NOTICES=0
+
+ # Get unique workshop paths from changed English files
+ for file in ${{ steps.changed.outputs.all_changed_files }}; do
+ if [[ "$file" == content/english/* ]]; then
+ # Extract workshop name (first directory after english/)
+ WORKSHOP=$(echo "$file" | sed 's|content/english/||' | cut -d'/' -f1)
+
+ for lang in "${LANGUAGES[@]}"; do
+ LANG_DIR="content/$lang/$WORKSHOP"
+ if [ -d "$LANG_DIR" ]; then
+ echo "đ Workshop '$WORKSHOP' also exists in $lang â please verify changes apply there too."
+ NOTICES=$((NOTICES + 1))
+ break # Only report once per workshop
+ fi
+ done
+ fi
+ done
+
+ if [ $NOTICES -gt 0 ]; then
+ echo ""
+ echo "âšī¸ $NOTICES workshop(s) have translations that may need matching updates."
+ echo " If this is a structural change (TOC, layout, images), update all languages."
+ echo " If this is English-only content, you can skip this."
+ else
+ echo "â
No translation parity issues detected."
+ fi
+ # Info only â don't fail the build
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000000..44f0b1f6c5
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,177 @@
+# Contributing to NuevoFoundation Workshops
+
+Thank you for helping us teach kids to code! đ This guide will help you make contributions that are easy to review and merge.
+
+## Quick start
+
+1. Fork the repo and create a feature branch from `master`
+2. Make your changes following the guidelines below
+3. Test locally with Hugo: `hugo server`
+4. Open a PR using the provided template
+
+## Workshop folder structure
+
+Every workshop lives under `content/{language}/{workshop-name}/`:
+
+```
+content/english/my-workshop/
+âââ _index.md # Landing page (title, description, prereqs)
+âââ activity-1.md # First activity
+âââ activity-2.md # Second activity
+âââ ...
+âââ answer-key/ # Optional answer keys (hidden from nav)
+â âââ _index.md
+âââ media/ # Images and assets for this workshop
+ âââ screenshot.png
+ âââ diagram.png
+```
+
+### Frontmatter template
+
+Every `_index.md` needs this YAML frontmatter:
+
+```yaml
+---
+title: "Workshop Title"
+description: "One sentence about what students will learn"
+date: 2026-01-01T00:00:00-00:00
+prereq: "Python Basics" # or "None"
+difficulty: "Beginner" # Beginner, Intermediate, Advanced
+draft: false
+icon: "fab fa-python" # Font Awesome icon
+---
+```
+
+Activity pages need:
+
+```yaml
+---
+title: "Activity 1: Getting Started"
+draft: false
+weight: 1 # Controls ordering
+---
+```
+
+### Table of contents pattern
+
+Workshop landing pages should use a collapsible TOC:
+
+```html
+
+Table of Contents
+{{% children /%}}
+
+```
+
+## Content guidelines
+
+### Images
+
+- **Lowercase extensions only**: `.png`, `.jpg`, `.gif` â never `.PNG` or `.JPG` (Linux servers are case-sensitive)
+- **Descriptive filenames**: `replit-upload-dialog.png` not `image1.png`
+- **Always include alt text**: ``
+- **Control image sizing**: `` or use HTML with `width` attribute
+- **Keep images under 500 KB** â compress screenshots with tools like [TinyPNG](https://tinypng.com)
+
+### HTML in markdown
+
+If you use raw HTML tags in markdown files:
+
+- Quote all attributes: `src="file.png"` not `src=file.png`
+- Close all tags properly
+- Include `alt` text on all `
` tags
+
+### YAML frontmatter
+
+- No duplicate keys (Hugo will silently use the last value)
+- Keep keys in English â only translate values
+- Use `draft: true` for work-in-progress content
+
+### Code blocks
+
+Use fenced code blocks with language hints:
+
+````markdown
+```python
+print("Hello, world!")
+```
+````
+
+## Translations
+
+We love translations! Here's how to do them well:
+
+### What to translate
+- All user-visible text (titles, descriptions, instructions, button labels)
+- YAML frontmatter values (`title`, `description`, `prereq`, `difficulty`)
+- Alt text on images
+
+### What NOT to translate
+- YAML frontmatter keys (`title:`, `weight:`, `draft:`)
+- Code inside code blocks
+- Hugo shortcodes (`{{% notice %}}`, `{{% children /%}}`)
+- File paths and image references
+- Variable names and function names
+
+### Translation checklist
+1. Create your files under `content/{language}/{workshop-name}/`
+2. Keep the same file structure as the English version
+3. If the workshop uses images with English text, note which ones need localized versions
+4. Test that Hugo builds successfully with your translation
+
+### Supported languages
+
+| Folder | Language |
+|--------|----------|
+| `english` | English |
+| `espanol` | Spanish |
+| `brazilian-portuguese` | Brazilian Portuguese |
+| `korean` | Korean |
+| `francais` | French |
+| `german` | German |
+| `simplified-chinese` | Simplified Chinese |
+| `traditional-chinese` | Traditional Chinese |
+| `kyrgyz` | Kyrgyz |
+
+## Testing locally
+
+### Prerequisites
+- [Hugo Extended](https://gohugo.io/installation/) (the extended version is required for SCSS)
+- Git
+
+### Run locally
+
+```bash
+git clone https://github.com/NuevoFoundation/workshops.git
+cd workshops
+git submodule update --init --recursive
+hugo server
+```
+
+Visit `http://localhost:1313` to preview.
+
+### Verify your changes
+
+Before opening a PR:
+
+```bash
+# Build the site â should complete with zero errors
+hugo --quiet
+
+# Check for uppercase image extensions
+grep -rn '\.\(PNG\|JPG\|GIF\|JPEG\)' content/ --include="*.md"
+
+# Check for unquoted HTML attributes (common in img tags)
+grep -rn 'src= [^"]' content/ --include="*.md"
+```
+
+## PR guidelines
+
+- Keep PRs focused: one fix or one feature per PR
+- Use the PR template checklist
+- If your PR fixes an issue, reference it: "Closes #123"
+- For translations: include all files for one complete workshop per PR
+
+## Questions?
+
+Open an issue or reach out to the maintainers. We're happy to help! đ