7 Statische Dateien

7.1 CSS- und JavaScript-Integration

Statische Dateien sind Ressourcen, die sich während der Anwendungsausführung nicht ändern. Dazu gehören CSS-Stylesheets, JavaScript-Dateien, Bilder, Schriftarten und andere Assets. Flask behandelt statische Dateien über einen speziellen Mechanismus, der diese Ressourcen effizient bereitstellt.

7.1.1 Verzeichnisstruktur für statische Dateien

Flask erwartet statische Dateien standardmäßig im static-Ordner. Eine organisierte Struktur erleichtert die Wartung und Entwicklung größerer Anwendungen.

projekt/
├── app.py
├── templates/
└── static/
    ├── css/
    │   ├── main.css
    │   ├── forms.css
    │   └── admin.css
    ├── js/
    │   ├── app.js
    │   ├── utils.js
    │   └── admin.js
    ├── images/
    │   ├── logo.png
    │   ├── banner.jpg
    │   └── icons/
    │       ├── user.svg
    │       └── settings.svg
    ├── fonts/
    │   ├── roboto.woff2
    │   └── opensans.woff2
    └── vendor/
        ├── bootstrap/
        └── jquery/

7.1.2 CSS-Integration in Templates

CSS-Dateien werden über die url_for-Funktion in Templates eingebunden. Diese Funktion generiert die korrekten URLs für statische Ressourcen und berücksichtigt dabei die Flask-Konfiguration.

<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}{% endblock %} - Firmenname</title>
    
    <!-- Bootstrap CSS über CDN -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    
    <!-- Eigene CSS-Dateien -->
    <link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
    
    <!-- Seitenspezifische CSS-Blöcke -->
    {% block extra_css %}{% endblock %}
</head>
<body>
    {% block content %}{% endblock %}
    
    <!-- JavaScript am Ende des Body für bessere Performance -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
    <script src="{{ url_for('static', filename='js/app.js') }}"></script>
    
    {% block extra_js %}{% endblock %}
</body>
</html>

7.1.3 CSS-Organisation und Struktur

Eine systematische CSS-Organisation verbessert die Wartbarkeit und Performance der Anwendung. Separate Dateien für verschiedene Bereiche ermöglichen gezieltes Laden und bessere Caching-Strategien.

/* static/css/main.css - Hauptstyles */
:root {
    --primary-color: #007bff;
    --secondary-color: #6c757d;
    --success-color: #28a745;
    --danger-color: #dc3545;
    --font-family-base: 'Roboto', sans-serif;
}

body {
    font-family: var(--font-family-base);
    line-height: 1.6;
    color: #333;
}

.navbar-brand {
    font-weight: bold;
}

.btn-custom {
    background-color: var(--primary-color);
    border-color: var(--primary-color);
    color: white;
}

.btn-custom:hover {
    background-color: #0056b3;
    border-color: #0056b3;
}

/* Responsive Utilities */
@media (max-width: 768px) {
    .navbar-brand {
        font-size: 1.1rem;
    }
}
/* static/css/forms.css - Formular-spezifische Styles */
.form-container {
    max-width: 600px;
    margin: 0 auto;
    padding: 2rem;
    background: #f8f9fa;
    border-radius: 8px;
}

.form-group {
    margin-bottom: 1.5rem;
}

.form-label {
    font-weight: 600;
    margin-bottom: 0.5rem;
    display: block;
}

.form-control:focus {
    border-color: var(--primary-color);
    box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}

.error-message {
    color: var(--danger-color);
    font-size: 0.875rem;
    margin-top: 0.25rem;
}

.success-message {
    color: var(--success-color);
    font-size: 0.875rem;
    margin-top: 0.25rem;
}

7.1.4 JavaScript-Integration und Modulstruktur

JavaScript-Dateien werden ähnlich wie CSS-Dateien eingebunden. Eine modulare Struktur ermöglicht bessere Code-Organisation und selektives Laden von Funktionalitäten.

// static/js/app.js - Hauptanwendungslogik
document.addEventListener('DOMContentLoaded', function() {
    // Flash-Nachrichten automatisch ausblenden
    const alerts = document.querySelectorAll('.alert');
    alerts.forEach(function(alert) {
        if (alert.classList.contains('alert-dismissible')) {
            setTimeout(function() {
                const closeButton = alert.querySelector('.btn-close');
                if (closeButton) {
                    closeButton.click();
                }
            }, 5000); // 5 Sekunden
        }
    });

    // Formular-Validierung
    const forms = document.querySelectorAll('form[data-validate="true"]');
    forms.forEach(function(form) {
        form.addEventListener('submit', function(event) {
            if (!form.checkValidity()) {
                event.preventDefault();
                event.stopPropagation();
            }
            form.classList.add('was-validated');
        });
    });

    // Bestätigungsdialoge
    const confirmButtons = document.querySelectorAll('[data-confirm]');
    confirmButtons.forEach(function(button) {
        button.addEventListener('click', function(event) {
            const message = button.getAttribute('data-confirm');
            if (!confirm(message)) {
                event.preventDefault();
            }
        });
    });
});

// Utility-Funktionen
window.AppUtils = {
    // CSRF-Token aus Meta-Tag holen
    getCsrfToken: function() {
        const token = document.querySelector('meta[name="csrf-token"]');
        return token ? token.getAttribute('content') : null;
    },

    // AJAX-Request mit CSRF-Schutz
    ajaxRequest: function(url, options = {}) {
        const defaultOptions = {
            headers: {
                'Content-Type': 'application/json',
                'X-CSRFToken': this.getCsrfToken()
            }
        };
        
        return fetch(url, Object.assign(defaultOptions, options));
    },

    // Toast-Benachrichtigung anzeigen
    showToast: function(message, type = 'info') {
        const toastContainer = document.getElementById('toast-container');
        if (!toastContainer) return;

        const toastHtml = `
            <div class="toast align-items-center text-white bg-${type} border-0" role="alert">
                <div class="d-flex">
                    <div class="toast-body">${message}</div>
                    <button type="button" class="btn-close btn-close-white me-2 m-auto" 
                            data-bs-dismiss="toast"></button>
                </div>
            </div>
        `;
        
        toastContainer.insertAdjacentHTML('beforeend', toastHtml);
        const toast = toastContainer.lastElementChild;
        const bsToast = new bootstrap.Toast(toast);
        bsToast.show();
    }
};

7.1.5 Seitenspezifisches JavaScript

Komplexere Anwendungen benötigen seitenspezifische JavaScript-Funktionalitäten, die nur bei Bedarf geladen werden.

// static/js/admin.js - Admin-spezifische Funktionen
document.addEventListener('DOMContentLoaded', function() {
    // Bulk-Aktionen für Admin-Listen
    const bulkActionForm = document.getElementById('bulk-actions');
    if (bulkActionForm) {
        const selectAllCheckbox = document.getElementById('select-all');
        const itemCheckboxes = document.querySelectorAll('input[name="selected_ids"]');
        
        selectAllCheckbox.addEventListener('change', function() {
            itemCheckboxes.forEach(function(checkbox) {
                checkbox.checked = selectAllCheckbox.checked;
            });
            updateBulkActionButtons();
        });

        itemCheckboxes.forEach(function(checkbox) {
            checkbox.addEventListener('change', updateBulkActionButtons);
        });

        function updateBulkActionButtons() {
            const checkedCount = document.querySelectorAll('input[name="selected_ids"]:checked').length;
            const bulkButtons = document.querySelectorAll('.bulk-action-btn');
            
            bulkButtons.forEach(function(button) {
                button.disabled = checkedCount === 0;
            });
        }
    }

    // Live-Suche für Admin-Tabellen
    const searchInput = document.getElementById('admin-search');
    if (searchInput) {
        let searchTimeout;
        
        searchInput.addEventListener('input', function() {
            clearTimeout(searchTimeout);
            searchTimeout = setTimeout(function() {
                performSearch(searchInput.value);
            }, 300);
        });

        function performSearch(query) {
            const tableRows = document.querySelectorAll('#admin-table tbody tr');
            
            tableRows.forEach(function(row) {
                const text = row.textContent.toLowerCase();
                const matches = text.includes(query.toLowerCase());
                row.style.display = matches ? '' : 'none';
            });
        }
    }
});

7.1.6 Template-Integration mit JavaScript

JavaScript kann dynamisch mit Template-Daten arbeiten, indem Daten über JSON oder HTML-Attribute übertragen werden.

<!-- templates/dashboard.html -->
{% extends "base.html" %}

{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}">
{% endblock %}

{% block content %}
<div class="container" data-user-id="{{ current_user.id }}" data-user-role="{{ current_user.role }}">
    <div class="row">
        <div class="col-md-12">
            <h1>Dashboard</h1>
            
            <!-- Daten für JavaScript bereitstellen -->
            <script type="application/json" id="dashboard-data">
                {
                    "statistiken": {{ statistiken|tojson|safe }},
                    "benachrichtigungen": {{ benachrichtigungen|tojson|safe }},
                    "einstellungen": {{ benutzer_einstellungen|tojson|safe }}
                }
            </script>
        </div>
    </div>
</div>
{% endblock %}

{% block extra_js %}
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
{% endblock %}
// static/js/dashboard.js
document.addEventListener('DOMContentLoaded', function() {
    // Template-Daten laden
    const dashboardDataElement = document.getElementById('dashboard-data');
    const dashboardData = JSON.parse(dashboardDataElement.textContent);
    
    // Statistiken rendern
    renderStatistiken(dashboardData.statistiken);
    
    // Benachrichtigungen anzeigen
    displayBenachrichtigungen(dashboardData.benachrichtigungen);
    
    function renderStatistiken(stats) {
        const statsContainer = document.getElementById('stats-container');
        
        Object.entries(stats).forEach(([key, value]) => {
            const statCard = document.createElement('div');
            statCard.className = 'col-md-3 mb-3';
            statCard.innerHTML = `
                <div class="card text-center">
                    <div class="card-body">
                        <h5 class="card-title">${value}</h5>
                        <p class="card-text">${key.replace('_', ' ')}</p>
                    </div>
                </div>
            `;
            statsContainer.appendChild(statCard);
        });
    }
    
    function displayBenachrichtigungen(notifications) {
        notifications.forEach(function(notification) {
            AppUtils.showToast(notification.text, notification.type);
        });
    }
});

7.2 Bilder und andere Medien

Mediendateien erfordern besondere Aufmerksamkeit hinsichtlich Performance, Zugänglichkeit und responsivem Design. Flask bietet verschiedene Strategien für die effiziente Verwaltung und Bereitstellung von Bildern und anderen Medieninhalten.

7.2.1 Bildorganisation und Bereitstellung

Eine strukturierte Bildorganisation verbessert die Wartbarkeit und ermöglicht effiziente Caching-Strategien.

static/
├── images/
│   ├── layout/
│   │   ├── logo.png
│   │   ├── logo@2x.png      # Retina-Version
│   │   ├── favicon.ico
│   │   └── banner.jpg
│   ├── products/
│   │   ├── thumbnails/
│   │   │   ├── produkt1-thumb.jpg
│   │   │   └── produkt2-thumb.jpg
│   │   └── full/
│   │       ├── produkt1.jpg
│   │       └── produkt2.jpg
│   ├── users/
│   │   └── avatars/
│   └── icons/
│       ├── social/
│       │   ├── facebook.svg
│       │   └── twitter.svg
│       └── ui/
│           ├── search.svg
│           └── menu.svg

7.2.2 Responsive Bilder und Performance

Moderne Webanwendungen müssen verschiedene Bildschirmgrößen und Auflösungen unterstützen. HTML5 bietet verschiedene Techniken für responsive Bilder.

<!-- templates/macros/image_helpers.html -->
{% macro responsive_image(src, alt, class="", sizes="") %}
    {% set base_path = src.rsplit('.', 1)[0] %}
    {% set extension = src.rsplit('.', 1)[1] %}
    
    <picture>
        <!-- WebP-Format für moderne Browser -->
        <source type="image/webp" 
                srcset="{{ url_for('static', filename=base_path + '.webp') }}" />
        
        <!-- Fallback für ältere Browser -->
        <img src="{{ url_for('static', filename=src) }}" 
             alt="{{ alt }}"
             class="{{ class }}"
             {% if sizes %}sizes="{{ sizes }}"{% endif %}
             loading="lazy" />
    </picture>
{% endmacro %}

{% macro product_image(product, size="medium") %}
    {% set size_map = {
        'small': 'thumbnails/',
        'medium': 'medium/',
        'large': 'full/'
    } %}
    
    {% set image_path = 'images/products/' + size_map[size] + product.image_filename %}
    
    <img src="{{ url_for('static', filename=image_path) }}" 
         alt="{{ product.name }}"
         class="product-image product-image-{{ size }}"
         loading="lazy" />
{% endmacro %}

7.2.3 CSS für Bildbehandlung

CSS spielt eine wichtige Rolle bei der korrekten Darstellung und Performance von Bildern.

/* static/css/images.css */

/* Responsive Bilder */
.responsive-image {
    max-width: 100%;
    height: auto;
    display: block;
}

/* Produktbilder */
.product-image {
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    transition: transform 0.3s ease;
}

.product-image:hover {
    transform: scale(1.02);
}

.product-image-small {
    width: 80px;
    height: 80px;
    object-fit: cover;
}

.product-image-medium {
    width: 200px;
    height: 200px;
    object-fit: cover;
}

.product-image-large {
    width: 100%;
    max-width: 500px;
    height: auto;
}

/* Avatar-Bilder */
.avatar {
    border-radius: 50%;
    border: 2px solid #e9ecef;
}

.avatar-small {
    width: 32px;
    height: 32px;
}

.avatar-medium {
    width: 64px;
    height: 64px;
}

.avatar-large {
    width: 128px;
    height: 128px;
}

/* Lazy Loading Placeholder */
.image-placeholder {
    background: linear-gradient(90deg, #f0f0f0 25%, transparent 37%, transparent 63%, #f0f0f0 75%);
    background-size: 400% 100%;
    animation: shimmer 1.5s ease-in-out infinite;
}

@keyframes shimmer {
    0% { background-position: 100% 0; }
    100% { background-position: -100% 0; }
}

/* Bildergalerien */
.image-gallery {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 1rem;
    margin: 2rem 0;
}

.gallery-item {
    position: relative;
    overflow: hidden;
    border-radius: 8px;
    aspect-ratio: 1;
}

.gallery-item img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform 0.3s ease;
}

.gallery-item:hover img {
    transform: scale(1.1);
}

.gallery-overlay {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
    color: white;
    padding: 1rem;
    transform: translateY(100%);
    transition: transform 0.3s ease;
}

.gallery-item:hover .gallery-overlay {
    transform: translateY(0);
}

7.2.4 SVG-Icons und Grafiken

SVG-Dateien bieten scharfe Grafiken bei jeder Auflösung und ermöglichen CSS-basierte Anpassungen.

<!-- templates/macros/icon_helpers.html -->
{% macro icon(name, class="", size="24") %}
    <svg class="icon icon-{{ name }} {{ class }}" 
         width="{{ size }}" height="{{ size }}"
         viewBox="0 0 24 24" fill="currentColor">
        <use href="{{ url_for('static', filename='images/icons/sprite.svg#' + name) }}"></use>
    </svg>
{% endmacro %}

{% macro social_icon(platform, url, class="") %}
    <a href="{{ url }}" class="social-link {{ class }}" target="_blank" rel="noopener">
        {{ icon(platform, 'social-icon') }}
        <span class="sr-only">{{ platform|title }}</span>
    </a>
{% endmacro %}
/* static/css/icons.css */
.icon {
    display: inline-block;
    vertical-align: middle;
    fill: currentColor;
}

.social-icon {
    width: 20px;
    height: 20px;
    transition: color 0.3s ease;
}

.social-link {
    color: #6c757d;
    text-decoration: none;
    margin: 0 0.5rem;
}

.social-link:hover {
    color: var(--primary-color);
}

/* Icon-spezifische Farben */
.social-link[href*="facebook.com"]:hover { color: #1877f2; }
.social-link[href*="twitter.com"]:hover { color: #1da1f2; }
.social-link[href*="linkedin.com"]:hover { color: #0077b5; }

7.2.5 Datei-Upload und -verwaltung

Hochgeladene Dateien erfordern besondere Behandlung hinsichtlich Sicherheit, Validierung und Speicherorganisation.

import os
import uuid
from werkzeug.utils import secure_filename
from PIL import Image

class FileManager:
    def __init__(self, upload_folder, allowed_extensions):
        self.upload_folder = upload_folder
        self.allowed_extensions = allowed_extensions
    
    def allowed_file(self, filename):
        return '.' in filename and \
               filename.rsplit('.', 1)[1].lower() in self.allowed_extensions
    
    def save_uploaded_file(self, file, subfolder=""):
        if not file or not self.allowed_file(file.filename):
            return None
        
        # Sicheren Dateinamen generieren
        filename = secure_filename(file.filename)
        unique_filename = f"{uuid.uuid4().hex}_{filename}"
        
        # Speicherpfad erstellen
        save_path = os.path.join(self.upload_folder, subfolder)
        os.makedirs(save_path, exist_ok=True)
        
        file_path = os.path.join(save_path, unique_filename)
        file.save(file_path)
        
        return unique_filename
    
    def create_thumbnail(self, image_path, thumbnail_path, size=(200, 200)):
        with Image.open(image_path) as img:
            img.thumbnail(size, Image.Resampling.LANCZOS)
            img.save(thumbnail_path, optimize=True, quality=85)

# Flask-Route für Datei-Upload
@app.route('/upload', methods=['POST'])
def upload_file():
    file_manager = FileManager(
        upload_folder=app.config['UPLOAD_FOLDER'],
        allowed_extensions={'png', 'jpg', 'jpeg', 'gif', 'webp'}
    )
    
    uploaded_file = request.files.get('file')
    if not uploaded_file:
        return jsonify({'error': 'Keine Datei ausgewählt'}), 400
    
    filename = file_manager.save_uploaded_file(uploaded_file, 'products')
    if not filename:
        return jsonify({'error': 'Ungültiger Dateityp'}), 400
    
    # Thumbnail erstellen
    original_path = os.path.join(app.config['UPLOAD_FOLDER'], 'products', filename)
    thumbnail_path = os.path.join(app.config['UPLOAD_FOLDER'], 'products', 'thumbnails', filename)
    file_manager.create_thumbnail(original_path, thumbnail_path)
    
    return jsonify({
        'filename': filename,
        'url': url_for('static', filename=f'uploads/products/{filename}'),
        'thumbnail_url': url_for('static', filename=f'uploads/products/thumbnails/{filename}')
    })

7.3 Asset-Management

Asset-Management umfasst die Organisation, Optimierung und Bereitstellung aller statischen Ressourcen einer Webanwendung. Effizientes Asset-Management verbessert Performance und Benutzerfreundlichkeit.

7.3.1 Datei-Versionierung und Caching

Browser-Caching verbessert die Performance, kann aber bei Updates zu Problemen führen. Datei-Versionierung löst dieses Problem durch eindeutige URLs bei Änderungen.

import hashlib
import os

class AssetManager:
    def __init__(self, static_folder):
        self.static_folder = static_folder
        self.file_hashes = {}
        self.generate_file_hashes()
    
    def generate_file_hashes(self):
        """Generiert Hashes für alle statischen Dateien"""
        for root, dirs, files in os.walk(self.static_folder):
            for file in files:
                file_path = os.path.join(root, file)
                relative_path = os.path.relpath(file_path, self.static_folder)
                
                with open(file_path, 'rb') as f:
                    file_hash = hashlib.md5(f.read()).hexdigest()[:8]
                    self.file_hashes[relative_path] = file_hash
    
    def versioned_url(self, filename):
        """Gibt versionierte URL für statische Datei zurück"""
        file_hash = self.file_hashes.get(filename, '')
        if file_hash:
            return f"{filename}?v={file_hash}"
        return filename

# Flask-Integration
asset_manager = AssetManager(app.static_folder)

@app.template_global()
def static_versioned(filename):
    """Template-Funktion für versionierte statische URLs"""
    versioned_filename = asset_manager.versioned_url(filename)
    return url_for('static', filename=versioned_filename)
<!-- Templates verwenden versionierte URLs -->
<link rel="stylesheet" href="{{ static_versioned('css/main.css') }}">
<script src="{{ static_versioned('js/app.js') }}"></script>

7.3.2 CSS- und JavaScript-Minifikation

Minifikation reduziert Dateigröße und verbessert Ladezeiten. Dies ist besonders wichtig für Produktionsumgebungen.

import cssmin
import jsmin

class AssetMinifier:
    def __init__(self, static_folder, output_folder):
        self.static_folder = static_folder
        self.output_folder = output_folder
    
    def minify_css_files(self, css_files, output_filename):
        """Kombiniert und minifiziert CSS-Dateien"""
        combined_css = ""
        
        for css_file in css_files:
            file_path = os.path.join(self.static_folder, css_file)
            if os.path.exists(file_path):
                with open(file_path, 'r', encoding='utf-8') as f:
                    combined_css += f.read() + "\n"
        
        # CSS minifizieren
        minified_css = cssmin.cssmin(combined_css)
        
        # Ausgabedatei schreiben
        output_path = os.path.join(self.output_folder, output_filename)
        os.makedirs(os.path.dirname(output_path), exist_ok=True)
        
        with open(output_path, 'w', encoding='utf-8') as f:
            f.write(minified_css)
        
        return output_filename
    
    def minify_js_files(self, js_files, output_filename):
        """Kombiniert und minifiziert JavaScript-Dateien"""
        combined_js = ""
        
        for js_file in js_files:
            file_path = os.path.join(self.static_folder, js_file)
            if os.path.exists(file_path):
                with open(file_path, 'r', encoding='utf-8') as f:
                    combined_js += f.read() + ";\n"
        
        # JavaScript minifizieren
        minified_js = jsmin.jsmin(combined_js)
        
        # Ausgabedatei schreiben
        output_path = os.path.join(self.output_folder, output_filename)
        os.makedirs(os.path.dirname(output_path), exist_ok=True)
        
        with open(output_path, 'w', encoding='utf-8') as f:
            f.write(minified_js)
        
        return output_filename

# Build-Prozess für Produktion
def build_assets():
    minifier = AssetMinifier(app.static_folder, app.static_folder)
    
    # CSS-Dateien kombinieren
    css_files = [
        'css/main.css',
        'css/forms.css',
        'css/components.css'
    ]
    minifier.minify_css_files(css_files, 'css/app.min.css')
    
    # JavaScript-Dateien kombinieren
    js_files = [
        'js/utils.js',
        'js/app.js',
        'js/components.js'
    ]
    minifier.minify_js_files(js_files, 'js/app.min.js')

# In Produktionsumgebung verwenden
if app.config['ENV'] == 'production':
    build_assets()

7.3.3 Asset-Pipeline und Build-Tools

Komplexere Anwendungen profitieren von Build-Tools, die Asset-Processing automatisieren. Hier ein Beispiel für eine einfache Python-basierte Asset-Pipeline.

# build_assets.py
import os
import shutil
import subprocess
from pathlib import Path

class AssetPipeline:
    def __init__(self, source_dir, output_dir):
        self.source_dir = Path(source_dir)
        self.output_dir = Path(output_dir)
        
    def clean_output(self):
        """Löscht Output-Verzeichnis"""
        if self.output_dir.exists():
            shutil.rmtree(self.output_dir)
        self.output_dir.mkdir(parents=True, exist_ok=True)
    
    def copy_assets(self):
        """Kopiert Assets zum Output-Verzeichnis"""
        # Bilder kopieren
        images_src = self.source_dir / 'images'
        images_dst = self.output_dir / 'images'
        if images_src.exists():
            shutil.copytree(images_src, images_dst)
        
        # Schriftarten kopieren
        fonts_src = self.source_dir / 'fonts'
        fonts_dst = self.output_dir / 'fonts'
        if fonts_src.exists():
            shutil.copytree(fonts_src, fonts_dst)
    
    def process_scss(self):
        """Kompiliert SCSS zu CSS"""
        scss_files = list(self.source_dir.glob('scss/**/*.scss'))
        css_output = self.output_dir / 'css'
        css_output.mkdir(exist_ok=True)
        
        for scss_file in scss_files:
            if not scss_file.name.startswith('_'):  # Keine Partials
                css_file = css_output / scss_file.with_suffix('.css').name
                
                # SCSS mit Sass kompilieren
                subprocess.run([
                    'sass',
                    str(scss_file),
                    str(css_file),
                    '--style=compressed'
                ], check=True)
    
    def optimize_images(self):
        """Optimiert Bilder für Web"""
        image_dir = self.output_dir / 'images'
        
        for image_file in image_dir.rglob('*.{jpg,jpeg,png}'):
            # Bildoptimierung mit Pillow
            try:
                with Image.open(image_file) as img:
                    # Progressive JPEG für bessere Ladezeiten
                    if image_file.suffix.lower() in ['.jpg', '.jpeg']:
                        img.save(image_file, 'JPEG', 
                                optimize=True, progressive=True, quality=85)
                    # PNG-Optimierung
                    elif image_file.suffix.lower() == '.png':
                        img.save(image_file, 'PNG', optimize=True)
            except Exception as e:
                print(f"Fehler bei Bildoptimierung {image_file}: {e}")
    
    def build(self):
        """Führt kompletten Build-Prozess aus"""
        print("Asset-Build gestartet...")
        
        self.clean_output()
        print("✓ Output-Verzeichnis bereinigt")
        
        self.copy_assets()
        print("✓ Assets kopiert")
        
        try:
            self.process_scss()
            print("✓ SCSS kompiliert")
        except subprocess.CalledProcessError:
            print("⚠ SCSS-Kompilierung fehlgeschlagen")
        
        self.optimize_images()
        print("✓ Bilder optimiert")
        
        print("Asset-Build abgeschlossen!")

if __name__ == '__main__':
    pipeline = AssetPipeline('static_src', 'static')
    pipeline.build()

7.4 Bootstrap-Integration

Bootstrap ist ein CSS-Framework, das vorgefertigte Komponenten und ein responsives Grid-System bietet. Die Integration in Flask-Anwendungen beschleunigt die Entwicklung und gewährleistet konsistente Benutzeroberflächen.

7.4.1 Bootstrap über CDN einbinden

Die einfachste Methode ist die Einbindung über Content Delivery Networks. Diese Methode ist schnell zu implementieren und nutzt Browser-Caching.

<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}{% endblock %} - Firma</title>
    
    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" 
          rel="stylesheet" 
          integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" 
          crossorigin="anonymous">
    
    <!-- Bootstrap Icons -->
    <link rel="stylesheet" 
          href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css">
    
    <!-- Custom CSS nach Bootstrap -->
    <link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}">
    
    {% block extra_css %}{% endblock %}
</head>
<body>
    {% block content %}{% endblock %}
    
    <!-- Bootstrap JavaScript Bundle -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" 
            integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" 
            crossorigin="anonymous"></script>
    
    {% block extra_js %}{% endblock %}
</body>
</html>

7.4.2 Lokale Bootstrap-Installation

Für Produktionsumgebungen oder Offline-Entwicklung kann Bootstrap lokal gespeichert werden.

# Bootstrap herunterladen und in static/vendor/ speichern
mkdir -p static/vendor/bootstrap
cd static/vendor/bootstrap
wget https://github.com/twbs/bootstrap/releases/download/v5.1.3/bootstrap-5.1.3-dist.zip
unzip bootstrap-5.1.3-dist.zip
<!-- Lokale Bootstrap-Dateien einbinden -->
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/bootstrap/css/bootstrap.min.css') }}">
<script src="{{ url_for('static', filename='vendor/bootstrap/js/bootstrap.bundle.min.js') }}"></script>

7.4.3 Bootstrap-Komponenten in Templates

Bootstrap bietet eine Vielzahl vorgefertigter Komponenten, die die Entwicklung beschleunigen.

<!-- templates/components/navigation.html -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
    <div class="container">
        <a class="navbar-brand" href="{{ url_for('index') }}">
            <i class="bi bi-building"></i>
            Firmenname
        </a>
        
        <button class="navbar-toggler" type="button" 
                data-bs-toggle="collapse" data-bs-target="#navbarNav">
            <span class="navbar-toggler-icon"></span>
        </button>
        
        <div class="collapse navbar-collapse" id="navbarNav">
            <ul class="navbar-nav me-auto">
                <li class="nav-item">
                    <a class="nav-link" href="{{ url_for('index') }}">Start</a>
                </li>
                <li class="nav-item dropdown">
                    <a class="nav-link dropdown-toggle" href="#" role="button" 
                       data-bs-toggle="dropdown">
                        Produkte
                    </a>
                    <ul class="dropdown-menu">
                        <li><a class="dropdown-item" href="{{ url_for('products.category', name='software') }}">Software</a></li>
                        <li><a class="dropdown-item" href="{{ url_for('products.category', name='hardware') }}">Hardware</a></li>
                        <li><hr class="dropdown-divider"></li>
                        <li><a class="dropdown-item" href="{{ url_for('products.all') }}">Alle Produkte</a></li>
                    </ul>
                </li>
            </ul>
            
            <ul class="navbar-nav">
                {% if current_user.is_authenticated %}
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" href="#" role="button" 
                           data-bs-toggle="dropdown">
                            <i class="bi bi-person-circle"></i>
                            {{ current_user.name }}
                        </a>
                        <ul class="dropdown-menu dropdown-menu-end">
                            <li><a class="dropdown-item" href="{{ url_for('profile') }}">Profil</a></li>
                            <li><a class="dropdown-item" href="{{ url_for('settings') }}">Einstellungen</a></li>
                            <li><hr class="dropdown-divider"></li>
                            <li><a class="dropdown-item" href="{{ url_for('logout') }}">Abmelden</a></li>
                        </ul>
                    </li>
                {% else %}
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('login') }}">
                            <i class="bi bi-box-arrow-in-right"></i>
                            Anmelden
                        </a>
                    </li>
                {% endif %}
            </ul>
        </div>
    </div>
</nav>

7.4.4 Bootstrap-Formulare

Bootstrap bietet umfassende Formular-Styling-Optionen, die mit Flask-WTF gut funktionieren.

<!-- templates/forms/contact.html -->
{% extends "base.html" %}

{% block content %}
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">
                    <h4 class="card-title mb-0">Kontakt aufnehmen</h4>
                </div>
                <div class="card-body">
                    <form method="POST" novalidate>
                        {{ form.hidden_tag() }}
                        
                        <div class="row">
                            <div class="col-md-6">
                                <div class="mb-3">
                                    {{ form.vorname.label(class="form-label") }}
                                    {{ form.vorname(class="form-control" + (" is-invalid" if form.vorname.errors else "")) }}
                                    {% for error in form.vorname.errors %}
                                        <div class="invalid-feedback">{{ error }}</div>
                                    {% endfor %}
                                </div>
                            </div>
                            <div class="col-md-6">
                                <div class="mb-3">
                                    {{ form.nachname.label(class="form-label") }}
                                    {{ form.nachname(class="form-control" + (" is-invalid" if form.nachname.errors else "")) }}
                                    {% for error in form.nachname.errors %}
                                        <div class="invalid-feedback">{{ error }}</div>
                                    {% endfor %}
                                </div>
                            </div>
                        </div>
                        
                        <div class="mb-3">
                            {{ form.email.label(class="form-label") }}
                            <div class="input-group">
                                <span class="input-group-text">
                                    <i class="bi bi-envelope"></i>
                                </span>
                                {{ form.email(class="form-control" + (" is-invalid" if form.email.errors else "")) }}
                                {% for error in form.email.errors %}
                                    <div class="invalid-feedback">{{ error }}</div>
                                {% endfor %}
                            </div>
                        </div>
                        
                        <div class="mb-3">
                            {{ form.betreff.label(class="form-label") }}
                            {{ form.betreff(class="form-select" + (" is-invalid" if form.betreff.errors else "")) }}
                            {% for error in form.betreff.errors %}
                                <div class="invalid-feedback">{{ error }}</div>
                            {% endfor %}
                        </div>
                        
                        <div class="mb-3">
                            {{ form.nachricht.label(class="form-label") }}
                            {{ form.nachricht(class="form-control" + (" is-invalid" if form.nachricht.errors else ""), rows="5") }}
                            {% for error in form.nachricht.errors %}
                                <div class="invalid-feedback">{{ error }}</div>
                            {% endfor %}
                            <div class="form-text">Ihre Nachricht wird vertraulich behandelt.</div>
                        </div>
                        
                        <div class="mb-3 form-check">
                            {{ form.datenschutz(class="form-check-input" + (" is-invalid" if form.datenschutz.errors else "")) }}
                            {{ form.datenschutz.label(class="form-check-label") }}
                            {% for error in form.datenschutz.errors %}
                                <div class="invalid-feedback">{{ error }}</div>
                            {% endfor %}
                        </div>
                        
                        <div class="d-grid gap-2 d-md-flex justify-content-md-end">
                            <button type="button" class="btn btn-secondary me-md-2" onclick="history.back()">
                                <i class="bi bi-arrow-left"></i>
                                Zurück
                            </button>
                            <button type="submit" class="btn btn-primary">
                                <i class="bi bi-send"></i>
                                Nachricht senden
                            </button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

7.4.5 Custom Bootstrap-Anpassungen

Bootstrap kann über CSS-Variablen und eigene Stylesheets an das Corporate Design angepasst werden.

/* static/css/custom.css */

/* Bootstrap-Variablen überschreiben */
:root {
    --bs-primary: #0d47a1;
    --bs-secondary: #37474f;
    --bs-success: #2e7d32;
    --bs-danger: #c62828;
    --bs-warning: #f57c00;
    --bs-info: #0277bd;
    
    /* Custom Variablen */
    --corporate-blue: #0d47a1;
    --corporate-gray: #37474f;
    --border-radius-custom: 8px;
}

/* Navbar-Anpassungen */
.navbar-brand {
    font-weight: 700;
    font-size: 1.5rem;
}

.navbar-nav .nav-link {
    font-weight: 500;
    padding: 0.75rem 1rem;
}

/* Button-Anpassungen */
.btn-primary {
    background-color: var(--corporate-blue);
    border-color: var(--corporate-blue);
    border-radius: var(--border-radius-custom);
}

.btn-primary:hover {
    background-color: #0a3d91;
    border-color: #0a3d91;
}

/* Card-Anpassungen */
.card {
    border-radius: var(--border-radius-custom);
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    border: none;
}

.card-header {
    background-color: var(--bs-light);
    border-bottom: 1px solid var(--bs-border-color);
    border-radius: var(--border-radius-custom) var(--border-radius-custom) 0 0;
}

/* Form-Anpassungen */
.form-control:focus,
.form-select:focus {
    border-color: var(--corporate-blue);
    box-shadow: 0 0 0 0.2rem rgba(13, 71, 161, 0.25);
}

/* Utility-Klassen */
.text-corporate {
    color: var(--corporate-blue) !important;
}

.bg-corporate {
    background-color: var(--corporate-blue) !important;
}

.border-corporate {
    border-color: var(--corporate-blue) !important;
}

/* Responsive Anpassungen */
@media (max-width: 768px) {
    .navbar-brand {
        font-size: 1.25rem;
    }
    
    .card {
        margin-bottom: 1rem;
    }
}

/* Print-Styles */
@media print {
    .navbar,
    .btn,
    .card-footer {
        display: none !important;
    }
    
    .card {
        border: 1px solid #000 !important;
        box-shadow: none !important;
    }
}

Diese umfassende Integration von Bootstrap mit Flask ermöglicht die schnelle Entwicklung professioneller Benutzeroberflächen, die responsive und benutzerfreundlich sind. Die Kombination aus Bootstrap-Komponenten und benutzerdefinierten CSS-Anpassungen bietet Flexibilität für spezifische Designanforderungen.