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.
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/
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>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;
}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();
}
};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';
});
}
}
});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);
});
}
});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.
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
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 %}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);
}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; }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}')
})Asset-Management umfasst die Organisation, Optimierung und Bereitstellung aller statischen Ressourcen einer Webanwendung. Effizientes Asset-Management verbessert Performance und Benutzerfreundlichkeit.
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>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()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()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.
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>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>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>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 %}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.