commit 8874c632626b2fc49a639d3b38e324b491b8e419 Author: Frank Schwenk Date: Fri Jun 12 17:17:52 2026 +0200 feat: tägliche KI-generierte Fake-Startup-Seite mit Archiv und Auflösungsseite MOBEA ist jeden Tag ein anderes Startup: Ein Generator-Container erzeugt 1x täglich via OpenRouter Texte und via Pixazo SDXL ein Hero-Bild, rendert statisches HTML nach public/ (nginx) und archiviert alle Ausgaben unter /archiv. Alle CTAs führen auf die Satire-Auflösung unter /echt-jetzt/. Co-authored-by: Cursor diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..93acadb --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +# OpenRouter (Text-Generierung, 1 Call/Tag) +OPENROUTER_API_KEY= +# frei wählbares Modell, z.B. ein günstiges +OPENROUTER_MODEL=openai/gpt-4o-mini +# Pixazo SDXL (Hero-Bild, kostenlos) - Ocp-Apim-Subscription-Key +PIXAZO_API_KEY= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e6bf0d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.env +__pycache__/ +data/ +public/index.html +public/archiv/ +public/img/ diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..be71d2b --- /dev/null +++ b/compose.yml @@ -0,0 +1,29 @@ +services: + + mobeade: + image: "nginx" + volumes: + - "./public:/usr/share/nginx/html:ro" + - "./nginx.conf:/etc/nginx/nginx.conf:ro" + container_name: "mobeade" + restart: always + labels: + - "traefik.enable=true" + - "traefik.http.routers.mobeade.rule=Host(`mobea.de`)||Host(`www.mobea.de`)" + - "traefik.http.routers.mobeade.entrypoints=websecure" + - "traefik.http.routers.mobeade.tls.certresolver=myresolver" + networks: + - traefik + + mobea-generator: + build: ./generator + container_name: mobea-generator + restart: always + env_file: .env + volumes: + - "./public:/site" + - "./data:/data" + +networks: + traefik: + external: true diff --git a/generator/Dockerfile b/generator/Dockerfile new file mode 100644 index 0000000..80e5341 --- /dev/null +++ b/generator/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.12-alpine + +WORKDIR /app + +ENV TZ=UTC \ + PYTHONUNBUFFERED=1 + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . +RUN chmod +x /app/entrypoint.sh + +ENTRYPOINT ["sh", "/app/entrypoint.sh"] diff --git a/generator/entrypoint.sh b/generator/entrypoint.sh new file mode 100755 index 0000000..25fb5db --- /dev/null +++ b/generator/entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# Kein `set -e`: ein Fehlschlag von generate.py darf die Schleife nicht beenden, +# der nächste Versuch erfolgt nach einer Stunde. + +while true; do + python /app/generate.py + exit_code=$? + if [ "$exit_code" -ne 0 ]; then + echo "$(date -u '+%Y-%m-%dT%H:%M:%SZ') generate.py exited with code $exit_code" + fi + sleep 3600 +done diff --git a/generator/generate.py b/generator/generate.py new file mode 100644 index 0000000..ca947be --- /dev/null +++ b/generator/generate.py @@ -0,0 +1,363 @@ +#!/usr/bin/env python3 +"""Daily mobea.de startup page generator.""" + +from __future__ import annotations + +import json +import os +import random +import re +import sys +from datetime import date, datetime, timezone +from pathlib import Path + +import requests +from jinja2 import Environment, FileSystemLoader + +import topics + +GERMAN_MONTHS = [ + "Januar", + "Februar", + "März", + "April", + "Mai", + "Juni", + "Juli", + "August", + "September", + "Oktober", + "November", + "Dezember", +] + +THEMES = ["corporate", "neon", "mystic", "brutalist", "pastel"] + +REQUIRED_LLM_KEYS = [ + "backronym_words", + "tagline", + "subtitle", + "pitch", + "features", + "pricing", + "testimonials", + "faq", + "cta", + "stats", + "image_prompt", + "image_negative_prompt", +] + +LLM_SCHEMA = """{ + "backronym_words": ["Modulare", "Orbitale", "Bewusstseins-", "Energie-", "Allianz"], + "tagline": "kurzer knackiger Claim", + "subtitle": "1-2 Sätze Hero-Untertitel", + "pitch": "Absatz 'Über uns', 3-5 Sätze, völlig ernst im Ton", + "features": [{"icon": "🔮", "title": "...", "text": "1-2 Sätze"}], + "pricing": [{"name": "Starter", "price": "9,99 €", "period": "/Monat", "features": ["...","...","..."], "cta": "Jetzt starten"}], + "testimonials": [{"quote": "...", "author": "Vorname N.", "role": "Beruf, Stadt"}], + "faq": [{"q": "...", "a": "..."}], + "cta": "Haupt-Button-Text", + "stats": [{"value": "98,3%", "label": "..."}], + "image_prompt": "english SDXL prompt for the hero image, no text in image", + "image_negative_prompt": "english negative prompt" +}""" + +SCRIPT_DIR = Path(__file__).resolve().parent + + +def log(message: str) -> None: + ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + print(f"{ts} {message}", flush=True) + + +def date_human(day: date) -> str: + return f"{day.day}. {GERMAN_MONTHS[day.month - 1]} {day.year}" + + +def load_archive(data_dir: Path) -> list[dict]: + path = data_dir / "archive.json" + if not path.exists(): + return [] + try: + data = json.loads(path.read_text(encoding="utf-8")) + if isinstance(data, list): + return data + log(f"warning: {path} is not a list, starting fresh") + except (json.JSONDecodeError, OSError) as exc: + log(f"warning: could not read {path}: {exc}, starting fresh") + return [] + + +def save_json_atomic(path: Path, data: object) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + tmp = path.with_suffix(path.suffix + ".tmp") + tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + os.replace(tmp, path) + + +def write_text_atomic(path: Path, content: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + tmp = path.with_suffix(path.suffix + ".tmp") + tmp.write_text(content, encoding="utf-8") + os.replace(tmp, path) + + +def strip_json_fences(text: str) -> str: + stripped = text.strip() + if stripped.startswith("```"): + stripped = re.sub(r"^```(?:json)?\s*", "", stripped) + stripped = re.sub(r"\s*```$", "", stripped) + return stripped.strip() + + +def validate_llm_json(data: dict) -> None: + missing = [key for key in REQUIRED_LLM_KEYS if key not in data] + if missing: + raise ValueError(f"missing required keys: {', '.join(missing)}") + + if len(data.get("backronym_words", [])) != 5: + log("warning: backronym_words should contain exactly 5 words") + + features = data.get("features", []) + if not (3 <= len(features) <= 4): + log(f"warning: expected 3-4 features, got {len(features)}") + + pricing = data.get("pricing", []) + if len(pricing) != 3: + log(f"warning: expected exactly 3 pricing tiers, got {len(pricing)}") + elif pricing[-1].get("name") != "Enterprise": + log("warning: last pricing tier should be named 'Enterprise'") + elif pricing[-1].get("price") != "Auf Anfrage": + log("warning: Enterprise tier price should be 'Auf Anfrage'") + elif pricing[-1].get("cta") != "Kontaktieren Sie uns": + log("warning: Enterprise tier cta should be 'Kontaktieren Sie uns'") + + testimonials = data.get("testimonials", []) + if not (2 <= len(testimonials) <= 3): + log(f"warning: expected 2-3 testimonials, got {len(testimonials)}") + + faq = data.get("faq", []) + if not (3 <= len(faq) <= 4): + log(f"warning: expected 3-4 faq entries, got {len(faq)}") + + stats = data.get("stats", []) + if len(stats) != 3: + log(f"warning: expected exactly 3 stats, got {len(stats)}") + + +def build_system_prompt() -> str: + return ( + "Du bist Texter für eine Satire-Website namens mobea.de – " + "'das Startup, das jeden Tag ein anderes Startup ist'. " + "Schreibe ausschließlich auf Deutsch (außer image_prompt und image_negative_prompt). " + "Der Humor entsteht durch absurde Inhalte in völlig ernstem Startup-Marketing-Ton. " + "Antworte ausschließlich mit validem JSON nach diesem Schema:\n" + f"{LLM_SCHEMA}\n\n" + "Pflichtregeln: backronym_words = genau 5 Wörter, deren Anfangsbuchstaben " + "zusammen MOBEA ergeben; 3-4 features; genau 3 pricing tiers, der letzte " + "name 'Enterprise', price 'Auf Anfrage', cta 'Kontaktieren Sie uns'; " + "2-3 testimonials; 3-4 faq; genau 3 stats." + ) + + +def build_user_prompt(selected_topics: list[dict]) -> str: + lines = ["Themen des Tages:"] + for topic in selected_topics: + lines.append(f"- {topic['name']}: {topic['hint']}") + lines.append( + "Erfinde ein völlig neues, absurd-satirisches Startup im todernsten " + "Marketing-Ton. Alle Texte müssen wie echtes Startup-Deutsch klingen." + ) + return "\n".join(lines) + + +def call_openrouter(api_key: str, model: str, selected_topics: list[dict]) -> dict: + payload = { + "model": model, + "temperature": 0.9, + "response_format": {"type": "json_object"}, + "messages": [ + {"role": "system", "content": build_system_prompt()}, + {"role": "user", "content": build_user_prompt(selected_topics)}, + ], + } + response = requests.post( + "https://openrouter.ai/api/v1/chat/completions", + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + }, + json=payload, + timeout=120, + ) + response.raise_for_status() + body = response.json() + content = body["choices"][0]["message"]["content"] + data = json.loads(strip_json_fences(content)) + validate_llm_json(data) + return data + + +def generate_hero_image( + api_key: str, + site_dir: Path, + date_iso: str, + prompt: str, + negative_prompt: str, +) -> str | None: + try: + response = requests.post( + "https://gateway.pixazo.ai/getImage/v1/getSDXLImage", + headers={ + "Content-Type": "application/json", + "Cache-Control": "no-cache", + "Ocp-Apim-Subscription-Key": api_key, + }, + json={ + "prompt": prompt, + "negative_prompt": negative_prompt, + "height": 1024, + "width": 1024, + "num_steps": 20, + "guidance_scale": 5, + }, + timeout=180, + ) + response.raise_for_status() + body = response.json() + image_url = body.get("imageUrl") or body.get("output") + if not image_url: + log("warning: Pixazo response missing imageUrl/output") + return None + + img_response = requests.get(image_url, timeout=120) + img_response.raise_for_status() + + img_dir = site_dir / "img" + img_dir.mkdir(parents=True, exist_ok=True) + img_path = img_dir / f"{date_iso}.jpg" + img_path.write_bytes(img_response.content) + return f"/img/{date_iso}.jpg" + except (requests.RequestException, OSError, KeyError, TypeError, ValueError) as exc: + log(f"warning: hero image generation failed: {exc}") + return None + + +def render_template(name: str, context: dict) -> str: + env = Environment( + loader=FileSystemLoader(SCRIPT_DIR / "templates"), + autoescape=True, + ) + return env.get_template(name).render(**context) + + +def build_archiv_entries(archive: list[dict]) -> list[dict]: + entries = [] + for item in sorted(archive, key=lambda e: e["date"], reverse=True): + entries.append( + { + "date": item["date"], + "date_human": item["date_human"], + "backronym": item["backronym"], + "tagline": item["tagline"], + "url": f"/archiv/{item['date']}.html", + "image": f"/img/{item['date']}.jpg" if item.get("has_image") else None, + "topics": item.get("topics", []), + } + ) + return entries + + +def main() -> int: + api_key = os.environ.get("OPENROUTER_API_KEY") + model = os.environ.get("OPENROUTER_MODEL") + pixazo_key = os.environ.get("PIXAZO_API_KEY") + site_dir = Path(os.environ.get("SITE_DIR", "/site")) + data_dir = Path(os.environ.get("DATA_DIR", "/data")) + generate_hour = int(os.environ.get("GENERATE_HOUR_UTC", "5")) + + if not api_key: + log("error: OPENROUTER_API_KEY is required") + return 1 + if not model: + log("error: OPENROUTER_MODEL is required") + return 1 + + now = datetime.now(timezone.utc) + today = now.date() + date_iso = today.isoformat() + index_path = site_dir / "index.html" + + archive = load_archive(data_dir) + if any(entry.get("date") == date_iso for entry in archive) and index_path.exists(): + log("up to date") + return 0 + + if index_path.exists() and now.hour < generate_hour: + log(f"waiting until {generate_hour:02d}:00 UTC (current hour: {now.hour})") + return 0 + + selected_topics = topics.pick_topics(today) + theme = random.Random(f"theme-{date_iso}").choice(THEMES) + log(f"generating edition for {date_iso} theme={theme} topics={[t['name'] for t in selected_topics]}") + + try: + llm_json = call_openrouter(api_key, model, selected_topics) + except (requests.RequestException, json.JSONDecodeError, KeyError, ValueError) as exc: + log(f"error: OpenRouter call failed: {exc}") + return 1 + + hero_image = None + if pixazo_key: + hero_image = generate_hero_image( + pixazo_key, + site_dir, + date_iso, + llm_json["image_prompt"], + llm_json["image_negative_prompt"], + ) + else: + log("PIXAZO_API_KEY not set, skipping hero image") + + human_date = date_human(today) + startup_context = { + "date": date_iso, + "date_human": human_date, + "theme": theme, + "hero_image": hero_image, + "s": llm_json, + } + startup_html = render_template("startup.html.j2", startup_context) + + archiv_day_path = site_dir / "archiv" / f"{date_iso}.html" + write_text_atomic(index_path, startup_html) + write_text_atomic(archiv_day_path, startup_html) + log(f"wrote {index_path} and {archiv_day_path}") + + archive_entry = { + "date": date_iso, + "date_human": human_date, + "backronym": " ".join(llm_json["backronym_words"]), + "tagline": llm_json["tagline"], + "theme": theme, + "topics": [topic["name"] for topic in selected_topics], + "has_image": hero_image is not None, + } + archive = [entry for entry in archive if entry.get("date") != date_iso] + archive.append(archive_entry) + save_json_atomic(data_dir / "archive.json", archive) + log(f"updated {data_dir / 'archive.json'}") + + archiv_html = render_template( + "archiv.html.j2", + {"entries": build_archiv_entries(archive)}, + ) + write_text_atomic(site_dir / "archiv" / "index.html", archiv_html) + log(f"wrote {site_dir / 'archiv' / 'index.html'}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/generator/requirements.txt b/generator/requirements.txt new file mode 100644 index 0000000..7f541b1 --- /dev/null +++ b/generator/requirements.txt @@ -0,0 +1,2 @@ +requests==2.32.3 +jinja2==3.1.6 diff --git a/generator/templates/archiv.html.j2 b/generator/templates/archiv.html.j2 new file mode 100644 index 0000000..b4bf949 --- /dev/null +++ b/generator/templates/archiv.html.j2 @@ -0,0 +1,270 @@ + + + + + + MOBEA Archiv – Unsere bisherigen Pivots + + + + + + +
+
+ + +
+
+ +
+
+
+

Unsere bisherigen Pivots

+

Jeden Tag ein neues Startup. Hier ruhen die bisherigen.

+
+ + {% if entries %} + + {% else %} +

Noch keine Pivots. Komm morgen wieder.

+ {% endif %} +
+
+ + + + + diff --git a/generator/templates/startup.html.j2 b/generator/templates/startup.html.j2 new file mode 100644 index 0000000..1dbd3f1 --- /dev/null +++ b/generator/templates/startup.html.j2 @@ -0,0 +1,1014 @@ + + + + + + MOBEA – {{ s.tagline }} + + + + {% set t = theme %} + {% if t == 'corporate' %} + + {% elif t == 'neon' %} + + {% elif t == 'mystic' %} + + {% elif t == 'brutalist' %} + + {% elif t == 'pastel' %} + + {% else %} + + {% endif %} + + + + +
+
+ + + +
+
+ +
+
+
+
+
+ {% for word in s.backronym_words %} + {{ word[1:] }} + {% endfor %} +
+

{{ s.tagline }}

+

{{ s.subtitle }}

+ {{ s.cta }} +
+
+ {% if hero_image %} + MOBEA – {{ s.tagline }} + {% else %} + + {% endif %} +
+
+
+ +
+
+ {% for stat in s.stats %} +
+
{{ stat.value }}
+
{{ stat.label }}
+
+ {% endfor %} +
+
+ +
+
+
+

Was wir bieten

+

Enterprise-ready. Skalierbar. Irgendwie.

+
+
+ {% for f in s.features %} +
+ +

{{ f.title }}

+

{{ f.text }}

+
+ {% endfor %} +
+
+
+ +
+
+
+

Über uns

+

{{ s.pitch }}

+
+
+
+ +
+
+
+

Transparente Preise

+

Keine versteckten Kosten. Nur versteckte Absichten.

+
+
+ {% for tier in s.pricing %} +
+ {% if loop.index == 2 %}Beliebt{% endif %} +

{{ tier.name }}

+
{{ tier.price }}
+
{{ tier.period }}
+
    + {% for feat in tier.features %} +
  • {{ feat }}
  • + {% endfor %} +
+ {{ tier.cta }} +
+ {% endfor %} +
+
+
+ +
+
+
+

Was unsere Kunden sagen

+

100 % authentisch. 0 % überprüfbar.

+
+
+ {% for t in s.testimonials %} +
+

{{ t.quote }}

+
+
{{ t.author }}
+
{{ t.role }}
+
+
+ {% endfor %} +
+
+
+ +
+
+
+

Häufige Fragen

+

Alles, was Sie wissen müssen. Und ein bisschen mehr.

+
+
+ {% for item in s.faq %} +
+ {{ item.q }} +
{{ item.a }}
+
+ {% endfor %} +
+
+
+ +
+
+
+

Bereit für den nächsten Pivot?

+

Schließen Sie sich Tausenden zufriedener Nutzer an, die es auch nicht sind.

+ {{ s.cta }} +
+
+
+
+ + + + + + + diff --git a/generator/topics.py b/generator/topics.py new file mode 100644 index 0000000..cd91541 --- /dev/null +++ b/generator/topics.py @@ -0,0 +1,52 @@ +import random +from datetime import date + +TOPICS = [ + { + "name": "Tech-Buzzwords", + "hint": "KI, Blockchain, Quantum, Web3, Synergie – alles buzzword-lastig und völlig ernst", + }, + { + "name": "Esoterik", + "hint": "Chakren, Mondphasen, Energiefelder, Schwingungen – spirituell-korporativ", + }, + { + "name": "Verschwörungstheorien", + "hint": "harmlos-albern, KEINE echten Personen/Gruppen/Ereignisse, eher Tauben-sind-Drohnen-Niveau", + }, + { + "name": "Wellness/Biohacking", + "hint": "Optimierung, Detox, Longevity, quantifiziertes Selbst", + }, + { + "name": "Coaching/MLM-Sprech", + "hint": "Mindset, Erfolg, passive Einkommensströme, du schaffst das", + }, + { + "name": "Greenwashing/Nachhaltigkeit", + "hint": "klimaneutral, CO₂-Kompensation, planet-positive – ohne Substanz", + }, + { + "name": "Pet-Tech", + "hint": "Smart Collar, KI-Hundefutter, quantifizierte Haustier-Liebe", + }, + { + "name": "Behörden-Digitalisierung", + "hint": "E-Akte, Bürgerportal, digitale Souveränität – bürokratisch-absurd", + }, + { + "name": "Dating", + "hint": "Matching-Algorithmen, Love-as-a-Service, Swipe-Optimierung", + }, + { + "name": "Krypto", + "hint": "DeFi, Tokenomics, Web3-Infrastruktur – revolutionär und ernst", + }, +] + + +def pick_topics(day: date) -> list[dict]: + """Return 1–2 topic dicts, deterministic for the given UTC date.""" + rng = random.Random(day.isoformat()) + count = rng.choice([1, 2]) + return rng.sample(TOPICS, count) diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..d67c84d --- /dev/null +++ b/nginx.conf @@ -0,0 +1,174 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + # Gzip Settings + gzip on; # Enabled gzip + gzip_disable "msie6"; # no gzip for IE6 + + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types + application/atom+xml + application/javascript + application/json + application/ld+json + application/manifest+json + application/rss+xml + application/vnd.geo+json + application/vnd.ms-fontobject + application/x-font-ttf + application/x-web-app-manifest+json + application/xhtml+xml + application/xml + font/opentype + image/bmp + image/svg+xml + image/x-icon + text/cache-manifest + text/css + text/plain + text/vcard + text/vnd.rim.location.xloc + text/vtt + text/x-component + text/x-cross-domain-policy; + + server_tokens off; # Hide Nginx version for security + + server { + listen 80; + listen [::]:80; + + # For SSL, uncomment and configure the lines below + # listen 443 ssl http2; + # listen [::]:443 ssl http2; + # ssl_certificate /path/to/your/fullchain.pem; # EDIT THIS + # ssl_certificate_key /path/to/your/privkey.pem; # EDIT THIS + + # Recommended SSL settings + # ssl_protocols TLSv1.2 TLSv1.3; + # ssl_prefer_server_ciphers on; + # ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; + # ssl_session_cache shared:SSL:10m; + # ssl_session_timeout 1d; + # ssl_session_tickets off; + + # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) + # add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + + # OCSP Stapling + # ssl_stapling on; + # ssl_stapling_verify on; + # resolver 8.8.8.8 8.8.4.4 valid=300s; # Use your preferred DNS resolvers + # resolver_timeout 5s; + + server_name mobea.de www.mobea.de; + root /usr/share/nginx/html; + + index index.html; + + location = /favicon.ico { + log_not_found off; + access_log off; + } + + location = /robots.txt { + allow all; + log_not_found off; + access_log off; + } + + # Cache static assets + location ~* \.(?:css|js)$ { + expires 1y; + add_header Cache-Control "public, max-age=31536000, immutable, no-transform"; + access_log off; + } + + location ~* \.(?:jpg|jpeg|gif|png|webp|svg|ico|woff|woff2|ttf|eot|otf)$ { + expires 1y; + add_header Cache-Control "public, max-age=31536000, immutable, no-transform"; + access_log off; + + # Hotlink prevention + # Allow your own domain, subdomains, common search engines, and direct access (none) + # You can add more domains to the list if needed, e.g. ~.anothercdn.com + valid_referers none blocked server_names + ~\.google\. ~.bing\. ~.yahoo\. ~.duckduckgo\.; # Search engines + + if ($invalid_referer) { + # Consider serving a placeholder image instead of 403 if preferred + # rewrite ^/images/(.*)$ /images/hotlink-placeholder.png last; + return 403; # Forbidden + } + } + + # Prevent access to .hidden files and directories (e.g., .git) + location ~ /\. { + deny all; + log_not_found off; + access_log off; + } + + # Deny access to .sh files + location ~* \.sh$ { + deny all; + log_not_found off; + access_log off; + } + + # Deny access to .md files (like TODO.md) + location ~* \.md$ { + deny all; + log_not_found off; + access_log off; + } + + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-cache, must-revalidate"; + } + + # Error pages (optional, create these files or Nginx uses defaults) + # error_page 403 /403.html; + # error_page 404 /404.html; + # location = /40x.html { # A generic page for 403 and 404 + # root /var/www/errors; # Path to your error pages + # internal; + # } + # error_page 500 502 503 504 /50x.html; + # location = /50x.html { + # root /var/www/errors; # Path to your error pages + # internal; + # } + } + + include /etc/nginx/conf.d/*.conf; +} diff --git a/public/echt-jetzt/index.html b/public/echt-jetzt/index.html new file mode 100644 index 0000000..c0abec9 --- /dev/null +++ b/public/echt-jetzt/index.html @@ -0,0 +1,244 @@ + + + + + + + Echt jetzt? – MOBEA + + + +
+ + +

Oh. Du hast das wirklich geglaubt?

+ +

+ MOBEA ist kein Startup. MOBEA ist jeden Tag ein anderes Startup – + eine täglich von einer KI neu erfundene, todernst vorgetragene Fake-Firma. + Es gibt nichts zu kaufen, niemanden zu kontaktieren, und das Impressum führt … + nun ja, hierher. +

+ +
+

Wie funktioniert das?

+

+ Eine KI würfelt jeden Tag Themen – Tech-Buzzwords, Esoterik, harmlose Verschwörungen, + Coaching-Sprech und was sonst noch im Buzzword-Bingo steht – und erfindet dazu ein + Startup samt Preisen, Testimonials und Kennzahlen. Alles frei erfunden. +

+

+ Jede Ähnlichkeit mit echten Startups ist rein zufällig, aber statistisch + schwer vermeidbar. 😬 +

+
+ + + + + + +
+ + diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..f7b3911 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +User-agent: * +Disallow: /archiv +Disallow: /echt-jetzt