feat: Archiv-Backfill, responsive Bilder und Site-Meta

Ergänzt Backfill- und Hilfsskripte, erzeugt nach der Pixazo-Generierung
komprimierte srcset-Varianten, validiert leere Bilder und fügt Favicon,
Web Manifest sowie Open-Graph-/Twitter-Tags in die Templates ein.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Frank Schwenk
2026-06-12 18:14:40 +02:00
parent f6e901bad8
commit 8abd7ddb59
15 changed files with 668 additions and 10 deletions
+228
View File
@@ -0,0 +1,228 @@
#!/usr/bin/env python3
"""Generate missing archive editions for a date range."""
from __future__ import annotations
import argparse
import json
import os
import random
import sys
import time
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
import topics
from generate import (
THEMES,
build_archiv_entries,
build_system_prompt,
build_user_prompt,
call_openrouter,
date_human,
generate_hero_image,
load_archive,
log,
render_template,
save_json_atomic,
write_text_atomic,
)
def load_env_file(path: Path) -> None:
if not path.exists():
return
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
os.environ.setdefault(key.strip(), value.strip())
def parse_date(value: str) -> date:
return date.fromisoformat(value)
def generate_day(
day: date,
api_key: str,
model: str,
pixazo_key: str | None,
site_dir: Path,
data_dir: Path,
archive: list[dict],
*,
update_index: bool,
pixazo_pause: float,
) -> list[dict]:
date_iso = day.isoformat()
if any(entry.get("date") == date_iso for entry in archive):
log(f"skipping {date_iso}, already in archive")
return archive
selected_topics = topics.pick_topics(day)
theme = random.Random(f"theme-{date_iso}").choice(THEMES)
log(
f"generating edition for {date_iso} theme={theme} "
f"topics={[t['name'] for t in selected_topics]}"
)
llm_json = call_openrouter(api_key, model, selected_topics)
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"],
)
if pixazo_pause > 0:
log(f"pausing {pixazo_pause:.0f}s after Pixazo call")
time.sleep(pixazo_pause)
else:
log("PIXAZO_API_KEY not set, skipping hero image")
human_date = date_human(day)
site_url = os.environ.get("SITE_URL", "https://mobea.de")
startup_base = {
"date": date_iso,
"date_human": human_date,
"theme": theme,
"hero_image": hero_image,
"s": llm_json,
"site_url": site_url,
}
archiv_day_html = render_template(
"startup.html.j2",
{**startup_base, "canonical_path": f"/archiv/{date_iso}.html"},
)
archiv_day_path = site_dir / "archiv" / f"{date_iso}.html"
write_text_atomic(archiv_day_path, archiv_day_html)
log(f"wrote {archiv_day_path}")
if update_index:
startup_html = render_template(
"startup.html.j2",
{**startup_base, "canonical_path": "/"},
)
write_text_atomic(site_dir / "index.html", startup_html)
log(f"wrote {site_dir / 'index.html'}")
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,
"prompts": {
"content_system": build_system_prompt(),
"content_user": build_user_prompt(selected_topics),
"image_prompt": llm_json["image_prompt"],
"image_negative_prompt": llm_json["image_negative_prompt"],
},
}
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'}")
return archive
def main() -> int:
parser = argparse.ArgumentParser(description="Backfill mobea archive editions")
parser.add_argument("--from", dest="from_date", required=True, help="Start date (YYYY-MM-DD)")
parser.add_argument("--to", dest="to_date", default=None, help="End date (YYYY-MM-DD), default today UTC")
parser.add_argument("--site-dir", default=None)
parser.add_argument("--data-dir", default=None)
parser.add_argument("--env-file", default=None)
parser.add_argument(
"--pixazo-pause",
type=float,
default=30.0,
help="Seconds to wait after each Pixazo image generation (default: 30)",
)
args = parser.parse_args()
repo_root = Path(__file__).resolve().parent.parent
env_file = Path(args.env_file) if args.env_file else repo_root / ".env"
load_env_file(env_file)
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(args.site_dir or os.environ.get("SITE_DIR", repo_root / "public"))
data_dir = Path(args.data_dir or os.environ.get("DATA_DIR", repo_root / "data"))
if not api_key:
log("error: OPENROUTER_API_KEY is required")
return 1
if not model:
log("error: OPENROUTER_MODEL is required")
return 1
start = parse_date(args.from_date)
end = parse_date(args.to_date) if args.to_date else datetime.now(timezone.utc).date()
if start > end:
log("error: --from must not be after --to")
return 1
site_dir.mkdir(parents=True, exist_ok=True)
data_dir.mkdir(parents=True, exist_ok=True)
archive = load_archive(data_dir)
current = start
days: list[date] = []
while current <= end:
days.append(current)
current += timedelta(days=1)
log(f"backfill {start.isoformat()} .. {end.isoformat()} ({len(days)} day(s))")
for idx, day in enumerate(days):
try:
archive = generate_day(
day,
api_key,
model,
pixazo_key,
site_dir,
data_dir,
archive,
update_index=(idx == len(days) - 1),
pixazo_pause=args.pixazo_pause,
)
except Exception as exc:
log(f"error: failed for {day.isoformat()}: {exc}")
return 1
latest_image = next(
(
f"/img/{entry['date']}.jpg"
for entry in sorted(archive, key=lambda e: e["date"], reverse=True)
if entry.get("has_image")
),
"/og-default.jpg",
)
archiv_html = render_template(
"archiv.html.j2",
{
"entries": build_archiv_entries(archive, site_dir),
"site_url": os.environ.get("SITE_URL", "https://mobea.de"),
"og_image": latest_image,
},
)
write_text_atomic(site_dir / "archiv" / "index.html", archiv_html)
log(f"wrote {site_dir / 'archiv' / 'index.html'}")
log("backfill complete")
return 0
if __name__ == "__main__":
sys.exit(main())
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Create responsive image variants and patch existing HTML img tags."""
from __future__ import annotations
import argparse
import os
import re
import sys
from pathlib import Path
from generate import log, write_text_atomic
from images import create_responsive_variants, iter_source_images
HERO_IMG_RE = re.compile(
r'<img src="/img/(\d{4}-\d{2}-\d{2})\.jpg" alt="([^"]*)" width="560" height="420">'
)
THUMB_IMG_RE = re.compile(
r'<img src="/img/(\d{4}-\d{2}-\d{2})\.jpg" alt="([^"]*)" width="360" height="203" loading="lazy">'
)
def patch_html_images(path: Path, descriptors: dict[str, dict[str, str]]) -> bool:
html = path.read_text(encoding="utf-8")
original = html
def replace_hero(match: re.Match[str]) -> str:
date_iso, alt = match.group(1), match.group(2)
image = descriptors.get(date_iso)
if not image:
return match.group(0)
return (
f'<img src="{image["src"]}" srcset="{image["srcset"]}" sizes="{image["sizes"]}" '
f'alt="{alt}" width="560" height="420">'
)
def replace_thumb(match: re.Match[str]) -> str:
date_iso, alt = match.group(1), match.group(2)
image = descriptors.get(date_iso)
if not image:
return match.group(0)
return (
f'<img src="{image["thumb_src"]}" srcset="{image["thumb_srcset"]}" '
f'sizes="{image["thumb_sizes"]}" alt="{alt}" width="360" height="203" loading="lazy">'
)
html = HERO_IMG_RE.sub(replace_hero, html)
html = THUMB_IMG_RE.sub(replace_thumb, html)
if html != original:
write_text_atomic(path, html)
log(f"patched images in {path}")
return True
return False
def main() -> int:
parser = argparse.ArgumentParser(description="Create responsive hero image variants")
parser.add_argument("--site-dir", default=None)
args = parser.parse_args()
repo_root = Path(__file__).resolve().parent.parent
site_dir = Path(args.site_dir or os.environ.get("SITE_DIR", repo_root / "public"))
img_dir = site_dir / "img"
descriptors: dict[str, dict[str, str]] = {}
for source in iter_source_images(img_dir):
date_iso = source.stem
descriptor = create_responsive_variants(img_dir, date_iso)
if descriptor:
descriptors[date_iso] = descriptor
log(f"created variants for {date_iso}")
patched = 0
targets = [site_dir / "index.html", site_dir / "archiv" / "index.html"]
targets.extend(sorted((site_dir / "archiv").glob("2026-*.html")))
for path in targets:
if path.exists():
patched += patch_html_images(path, descriptors)
log(f"done, {len(descriptors)} image set(s), patched {patched} html file(s)")
return 0
if __name__ == "__main__":
sys.exit(main())
+38 -8
View File
@@ -15,6 +15,7 @@ import requests
from jinja2 import Environment, FileSystemLoader
import topics
from images import create_responsive_variants, load_image_descriptor
GERMAN_MONTHS = [
"Januar",
@@ -205,7 +206,7 @@ def generate_hero_image(
date_iso: str,
prompt: str,
negative_prompt: str,
) -> str | None:
) -> dict[str, str] | None:
try:
response = requests.post(
"https://gateway.pixazo.ai/getImage/v1/getSDXLImage",
@@ -233,12 +234,18 @@ def generate_hero_image(
img_response = requests.get(image_url, timeout=120)
img_response.raise_for_status()
if len(img_response.content) < 50_000:
log(
f"warning: hero image for {date_iso} too small "
f"({len(img_response.content)} bytes), discarding"
)
return None
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"
return create_responsive_variants(img_dir, date_iso)
except (requests.RequestException, OSError, KeyError, TypeError, ValueError) as exc:
log(f"warning: hero image generation failed: {exc}")
return None
@@ -252,7 +259,8 @@ def render_template(name: str, context: dict) -> str:
return env.get_template(name).render(**context)
def build_archiv_entries(archive: list[dict]) -> list[dict]:
def build_archiv_entries(archive: list[dict], site_dir: Path) -> list[dict]:
img_dir = site_dir / "img"
entries = []
for item in sorted(archive, key=lambda e: e["date"], reverse=True):
prompts = item.get("prompts")
@@ -263,7 +271,9 @@ def build_archiv_entries(archive: list[dict]) -> list[dict]:
"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,
"image": load_image_descriptor(img_dir, item["date"])
if item.get("has_image")
else None,
"topics": item.get("topics", []),
"prompts": prompts if isinstance(prompts, dict) else None,
}
@@ -323,18 +333,26 @@ def main() -> int:
log("PIXAZO_API_KEY not set, skipping hero image")
human_date = date_human(today)
startup_context = {
startup_base = {
"date": date_iso,
"date_human": human_date,
"theme": theme,
"hero_image": hero_image,
"s": llm_json,
"site_url": os.environ.get("SITE_URL", "https://mobea.de"),
}
startup_html = render_template("startup.html.j2", startup_context)
startup_html = render_template(
"startup.html.j2",
{**startup_base, "canonical_path": "/"},
)
archiv_day_html = render_template(
"startup.html.j2",
{**startup_base, "canonical_path": f"/archiv/{date_iso}.html"},
)
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)
write_text_atomic(archiv_day_path, archiv_day_html)
log(f"wrote {index_path} and {archiv_day_path}")
archive_entry = {
@@ -357,9 +375,21 @@ def main() -> int:
save_json_atomic(data_dir / "archive.json", archive)
log(f"updated {data_dir / 'archive.json'}")
latest_image = next(
(
f"/img/{entry['date']}.jpg"
for entry in sorted(archive, key=lambda e: e["date"], reverse=True)
if entry.get("has_image")
),
"/og-default.jpg",
)
archiv_html = render_template(
"archiv.html.j2",
{"entries": build_archiv_entries(archive)},
{
"entries": build_archiv_entries(archive, site_dir),
"site_url": os.environ.get("SITE_URL", "https://mobea.de"),
"og_image": latest_image,
},
)
write_text_atomic(site_dir / "archiv" / "index.html", archiv_html)
log(f"wrote {site_dir / 'archiv' / 'index.html'}")
+90
View File
@@ -0,0 +1,90 @@
"""Responsive JPEG variants for hero images."""
from __future__ import annotations
import re
from pathlib import Path
from PIL import Image
VARIANT_WIDTHS = (360, 560, 800)
JPEG_QUALITY = 82
DATE_IMAGE_RE = re.compile(r"^(\d{4}-\d{2}-\d{2})\.jpg$")
def image_public_path(date_iso: str, width: int | None = None) -> str:
if width is None:
return f"/img/{date_iso}.jpg"
return f"/img/{date_iso}-{width}w.jpg"
def build_srcset(date_iso: str, widths: list[int], full_width: int) -> str:
parts = [f"{image_public_path(date_iso, w)} {w}w" for w in widths]
parts.append(f"{image_public_path(date_iso)} {full_width}w")
return ", ".join(parts)
def create_responsive_variants(img_dir: Path, date_iso: str) -> dict[str, str] | None:
source = img_dir / f"{date_iso}.jpg"
if not source.exists():
return None
with Image.open(source) as im:
im = im.convert("RGB")
full_width, full_height = im.size
generated: list[int] = []
for width in VARIANT_WIDTHS:
if width >= full_width:
continue
height = round(full_height * width / full_width)
resized = im.resize((width, height), Image.Resampling.LANCZOS)
out = img_dir / f"{date_iso}-{width}w.jpg"
resized.save(out, "JPEG", quality=JPEG_QUALITY, optimize=True)
generated.append(width)
im.save(source, "JPEG", quality=JPEG_QUALITY, optimize=True)
srcset = build_srcset(date_iso, generated, full_width)
default_src = image_public_path(date_iso, 560) if 560 in generated else image_public_path(date_iso)
thumb_src = image_public_path(date_iso, 360) if 360 in generated else image_public_path(date_iso)
return {
"src": default_src,
"srcset": srcset,
"sizes": "(max-width: 768px) 100vw, 560px",
"og": image_public_path(date_iso),
"thumb_src": thumb_src,
"thumb_srcset": srcset,
"thumb_sizes": "(max-width: 640px) 50vw, 360px",
}
def load_image_descriptor(img_dir: Path, date_iso: str) -> dict[str, str] | None:
if not (img_dir / f"{date_iso}.jpg").exists():
return None
if not (img_dir / f"{date_iso}-360w.jpg").exists():
return create_responsive_variants(img_dir, date_iso)
widths = [w for w in VARIANT_WIDTHS if (img_dir / f"{date_iso}-{w}w.jpg").exists()]
with Image.open(img_dir / f"{date_iso}.jpg") as im:
full_width = im.size[0]
srcset = build_srcset(date_iso, widths, full_width)
return {
"src": image_public_path(date_iso, 560) if 560 in widths else image_public_path(date_iso),
"srcset": srcset,
"sizes": "(max-width: 768px) 100vw, 560px",
"og": image_public_path(date_iso),
"thumb_src": image_public_path(date_iso, 360) if 360 in widths else image_public_path(date_iso),
"thumb_srcset": srcset,
"thumb_sizes": "(max-width: 640px) 50vw, 360px",
}
def iter_source_images(img_dir: Path) -> list[Path]:
if not img_dir.exists():
return []
sources = []
for path in sorted(img_dir.glob("*.jpg")):
if DATE_IMAGE_RE.match(path.name):
sources.append(path)
return sources
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Inject favicon and social meta tags into existing static HTML pages."""
from __future__ import annotations
import json
import os
import re
import sys
from pathlib import Path
from generate import build_archiv_entries, log, render_template, write_text_atomic
def extract_meta(html: str, prop: str) -> str | None:
match = re.search(rf'property="{re.escape(prop)}" content="([^"]*)"', html)
return match.group(1) if match else None
def extract_name_meta(html: str, name: str) -> str | None:
match = re.search(rf'name="{re.escape(name)}" content="([^"]*)"', html)
return match.group(1) if match else None
def extract_hero_image(html: str) -> str | None:
match = re.search(r'<img src="/img/(\d{4}-\d{2}-\d{2})(?:-\d+w)?\.jpg"', html)
return f"/img/{match.group(1)}.jpg" if match else None
def render_head_block(
*,
site_url: str,
canonical_path: str,
og_title: str,
og_description: str,
og_image: str | None,
) -> str:
return render_template(
"_head_common.html.j2",
{
"site_url": site_url,
"canonical_path": canonical_path,
"og_title": og_title,
"og_description": og_description,
"og_image": og_image,
},
).rstrip()
def inject_head_block(html: str, block: str) -> str:
if 'rel="icon"' in html:
return html
marker = '<meta property="og:description"'
idx = html.find(marker)
if idx == -1:
marker = '<meta name="description"'
idx = html.find(marker)
if idx == -1:
raise ValueError("could not find description meta tag")
line_end = html.find("\n", idx)
if line_end == -1:
raise ValueError("unexpected HTML structure")
return html[: line_end + 1] + block + "\n" + html[line_end + 1 :]
def patch_startup_page(path: Path, canonical_path: str, site_url: str) -> bool:
html = path.read_text(encoding="utf-8")
og_title = extract_meta(html, "og:title") or extract_name_meta(html, "twitter:title")
og_description = extract_meta(html, "og:description") or extract_name_meta(html, "description")
if not og_title or not og_description:
log(f"warning: skipping {path}, missing title/description")
return False
block = render_head_block(
site_url=site_url,
canonical_path=canonical_path,
og_title=og_title,
og_description=og_description,
og_image=extract_hero_image(html),
)
updated = inject_head_block(html, block)
if updated != html:
write_text_atomic(path, updated)
log(f"patched {path}")
return True
return False
def patch_archiv_index(path: Path, site_url: str, og_image: str) -> bool:
html = path.read_text(encoding="utf-8")
og_title = "MOBEA Archiv Unsere bisherigen Pivots"
og_description = "Jeden Tag ein neues Startup. Hier ruhen die bisherigen."
if 'property="og:title"' not in html:
html = html.replace(
'<meta name="description" content="Jeden Tag ein neues Startup. Hier ruhen die bisherigen.">',
'<meta name="description" content="Jeden Tag ein neues Startup. Hier ruhen die bisherigen.">\n'
f' <meta property="og:title" content="{og_title}">\n'
f' <meta property="og:description" content="{og_description}">',
1,
)
block = render_head_block(
site_url=site_url,
canonical_path="/archiv/",
og_title=og_title,
og_description=og_description,
og_image=og_image,
)
updated = inject_head_block(html, block)
if updated != html:
write_text_atomic(path, updated)
log(f"patched {path}")
return True
return False
def main() -> int:
repo_root = Path(__file__).resolve().parent.parent
site_dir = Path(os.environ.get("SITE_DIR", repo_root / "public"))
data_dir = Path(os.environ.get("DATA_DIR", repo_root / "data"))
site_url = os.environ.get("SITE_URL", "https://mobea.de")
archive = json.loads((data_dir / "archive.json").read_text(encoding="utf-8"))
latest_image = next(
(
f"/img/{entry['date']}.jpg"
for entry in sorted(archive, key=lambda e: e["date"], reverse=True)
if entry.get("has_image")
),
"/og-default.jpg",
)
patched = 0
index_path = site_dir / "index.html"
if index_path.exists():
patched += patch_startup_page(index_path, "/", site_url)
archiv_dir = site_dir / "archiv"
for path in sorted(archiv_dir.glob("2026-*.html")):
patched += patch_startup_page(path, f"/archiv/{path.name}", site_url)
archiv_index = archiv_dir / "index.html"
if archiv_index.exists():
patched += patch_archiv_index(archiv_index, site_url, latest_image)
log(f"done, patched {patched} file(s)")
return 0
if __name__ == "__main__":
sys.exit(main())
+1
View File
@@ -1,2 +1,3 @@
requests==2.32.3
jinja2==3.1.6
pillow==11.2.1
+20
View File
@@ -0,0 +1,20 @@
{% set _site_url = site_url | default('https://mobea.de') %}
<link rel="icon" href="/favicon.svg" type="image/svg+xml" sizes="any">
<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="canonical" href="{{ _site_url }}{{ canonical_path }}">
<meta property="og:type" content="website">
<meta property="og:site_name" content="MOBEA">
<meta property="og:locale" content="de_DE">
<meta property="og:url" content="{{ _site_url }}{{ canonical_path }}">
{% if og_image %}
<meta property="og:image" content="{{ _site_url }}{{ og_image }}">
<meta property="og:image:alt" content="{{ og_title }}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="{{ _site_url }}{{ og_image }}">
{% else %}
<meta name="twitter:card" content="summary">
{% endif %}
<meta name="twitter:title" content="{{ og_title }}">
<meta name="twitter:description" content="{{ og_description }}">
+7 -1
View File
@@ -5,6 +5,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MOBEA Archiv Unsere bisherigen Pivots</title>
<meta name="description" content="Jeden Tag ein neues Startup. Hier ruhen die bisherigen.">
<meta property="og:title" content="MOBEA Archiv Unsere bisherigen Pivots">
<meta property="og:description" content="Jeden Tag ein neues Startup. Hier ruhen die bisherigen.">
{% set og_title = 'MOBEA Archiv Unsere bisherigen Pivots' %}
{% set og_description = 'Jeden Tag ein neues Startup. Hier ruhen die bisherigen.' %}
{% set canonical_path = '/archiv/' %}
{% include '_head_common.html.j2' %}
<meta name="theme-color" content="#111118">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@@ -314,7 +320,7 @@
<a href="{{ entry.url }}" class="entry-link">
<div class="entry-thumb">
{% if entry.image %}
<img src="{{ entry.image }}" alt="{{ entry.backronym }}" width="360" height="203" loading="lazy">
<img src="{{ entry.image.thumb_src }}" srcset="{{ entry.image.thumb_srcset }}" sizes="{{ entry.image.thumb_sizes }}" alt="{{ entry.backronym }}" width="360" height="203" loading="lazy">
{% endif %}
</div>
<div class="entry-body">
+5 -1
View File
@@ -7,6 +7,10 @@
<meta name="description" content="{{ s.subtitle }}">
<meta property="og:title" content="MOBEA {{ s.tagline }}">
<meta property="og:description" content="{{ s.subtitle }}">
{% set og_title = 'MOBEA ' ~ s.tagline %}
{% set og_description = s.subtitle %}
{% set og_image = hero_image.og if hero_image else none %}
{% include '_head_common.html.j2' %}
{% set t = theme %}
{% if t == 'corporate' %}
<meta name="theme-color" content="#2563eb">
@@ -859,7 +863,7 @@
</div>
<div class="hero-visual">
{% if hero_image %}
<img src="{{ hero_image }}" alt="MOBEA {{ s.tagline }}" width="560" height="420">
<img src="{{ hero_image.src }}" srcset="{{ hero_image.srcset }}" sizes="{{ hero_image.sizes }}" alt="MOBEA {{ s.tagline }}" width="560" height="420">
{% else %}
<div class="hero-gradient" aria-hidden="true">✦</div>
{% endif %}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

+18
View File
@@ -5,6 +5,24 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<title>Echt jetzt? MOBEA</title>
<meta name="description" content="MOBEA ist Satire. Jeden Tag ein neues fiktives Startup.">
<link rel="icon" href="/favicon.svg" type="image/svg+xml" sizes="any">
<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="canonical" href="https://mobea.de/echt-jetzt/">
<meta property="og:type" content="website">
<meta property="og:site_name" content="MOBEA">
<meta property="og:locale" content="de_DE">
<meta property="og:url" content="https://mobea.de/echt-jetzt/">
<meta property="og:title" content="Echt jetzt? MOBEA">
<meta property="og:description" content="MOBEA ist Satire. Jeden Tag ein neues fiktives Startup.">
<meta property="og:image" content="https://mobea.de/og-default.jpg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Echt jetzt? MOBEA">
<meta name="twitter:description" content="MOBEA ist Satire. Jeden Tag ein neues fiktives Startup.">
<meta name="twitter:image" content="https://mobea.de/og-default.jpg">
<meta name="theme-color" content="#12141a">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="MOBEA">
<rect width="32" height="32" rx="7" fill="#2563eb"/>
<text x="16" y="22" text-anchor="middle" fill="#ffffff" font-family="system-ui, sans-serif" font-size="15" font-weight="800">M</text>
</svg>

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

+23
View File
@@ -0,0 +1,23 @@
{
"name": "MOBEA",
"short_name": "MOBEA",
"description": "Das Startup, das jeden Tag ein anderes Startup ist.",
"start_url": "/",
"display": "standalone",
"background_color": "#111118",
"theme_color": "#2563eb",
"icons": [
{
"src": "/favicon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/apple-touch-icon.png",
"sizes": "180x180",
"type": "image/png",
"purpose": "any maskable"
}
]
}