Files
mesh-configs/skills/devops/web-hosting-deployment/references/multi-site-pattern.md
T

4.3 KiB

Multi-Site Pattern: Verlinkte Fitness/Calculator-Websites

Pattern aus Session 2026-07-12: Zwei zusammenhängende Single-Page-Apps auf Subdomains derselben Domain.

Überblick

  • fitness.perfekt-ohne-en.de: Makro-Rechner mit BMR, Aktivitätslevel, PDF-Export
  • kalorienrechner.perfekt-ohne-en.de: Reiner Kalorienrechner mit Abnehm-Prognose
  • Beide Sites per Cross-Link verbunden, Daten werden beim Wechsel übergeben

Architektur

perfekt-ohne-en.de (Hauptdomain)
├── fitness.perfekt-ohne-en.de     → /var/www/fitness.perfekt-ohne-en.de
└── kalorienrechner.perfekt-ohne-en.de → /var/www/kalorienrechner.perfekt-ohne-en.de

Datenübergabe per URL-Parameter

Wenn der Nutzer auf Fitness Daten eingibt und dann zum Kalorienrechner wechselt, werden die Formularwerte als Query-String mitgegeben:

// Auf der Quellseite:
function goToTarget(e) {
    e.preventDefault();
    const params = new URLSearchParams({
        g: gender,        // 'male'|'female'
        w: weight.value,  // kg
        h: height.value,  // cm
        a: age.value,
        act: activity.value,  // activity factor
        goal: goal.value      // goal factor
    });
    window.location.href = 'https://target.example.de/?' + params.toString();
}
<a href="#" onclick="goToTarget(event)">👉 Zur anderen Seite (mit Daten)</a>

Auf der Zielseite werden die Parameter beim Laden eingelesen und das Formular vorausgefüllt:

(function(){
    const p = new URLSearchParams(window.location.search);
    if (p.get('w')) document.getElementById('weight').value = p.get('w');
    if (p.get('h')) document.getElementById('height').value = p.get('h');
    if (p.get('a')) document.getElementById('age').value = p.get('a');
    if (p.get('g')) setGender(p.get('g'), ...);
    if (p.get('act')) document.getElementById('activity').value = p.get('act');
    if (p.get('goal')) document.getElementById('goal').value = p.get('goal');
    // Auto-calculate if all data present
    if (p.get('w') && p.get('h') && p.get('a')) calculate();
})();

URL-Beispiel:

https://kalorienrechner.perfekt-ohne-en.de/?g=male&w=80&h=178&a=30&act=1.375&goal=0.85

PDF-Download (jsPDF)

Beide Sites nutzen jsPDF (CDN) für PDF-Export:

<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>

Minimale downloadPDF()-Funktion:

async function downloadPDF() {
    if (typeof window.jspdf === 'undefined') return alert('PDF wird geladen...');
    const { jsPDF } = window.jspdf;
    const pdf = new jsPDF('p','mm','a4'), m = 20;
    pdf.setFillColor(16,185,129); pdf.rect(0,0,210,15,'F');
    pdf.setTextColor(255,255,255); pdf.setFontSize(14);
    pdf.text('Titel', m, 12);
    pdf.setTextColor(0,0,0); pdf.setFontSize(10);
    // ... Daten einfügen ...
    pdf.save('ergebnis.pdf');
}

Pitfall: pdf.text() existiert immer in jsPDF 2.x. Kein typeof-Check nötig!

Sponsor-Coin (CSS-Animation)

Kleines drehendes Logo mit Link zur Sponsoren-Seite:

.sponsor-coin { margin-bottom: 1rem; }
.coin-link { display: inline-flex; align-items: center; gap: 0.5rem; }
.coin-img {
    width: 45px; height: 45px; border-radius: 50%;
    animation: spinCoin 4s ease-in-out infinite;
    filter: drop-shadow(0 2px 6px rgba(16,185,129,0.3));
}
.coin-link:hover .coin-img {
    animation: spinCoin 0.6s ease-in-out infinite;
}
@keyframes spinCoin {
    0% { transform: rotateY(0deg); }
    50% { transform: rotateY(180deg); }
    100% { transform: rotateY(360deg); }
}
<div class="sponsor-coin">
    <a href="https://sponsor.example.de" class="coin-link">
        <img src="/assets/logo.png" alt="Sponsor" class="coin-img">
        <span class="coin-text">Gesponsert von</span>
    </a>
</div>

Deployment-Checkliste

  1. mkdir -p /var/www/sub.example.de/{html,logs,assets}
  2. nginx VHost in /etc/nginx/sites-available/ → ln -sf → sites-enabled/
  3. nginx -t && systemctl reload nginx
  4. HTML-Dateien erstellen → SOFORT: chown www-data:www-data && chmod 644
  5. Assets (Logos) kopieren → chown www-data:www-data
  6. certbot --nginx -d sub.example.de --non-interactive --agree-tos --email admin@... --redirect
  7. Lokal testen: curl -s -H "Host: sub.example.de" http://localhost/ | grep '<title>'