feat: Review-Builder auf Excel (.xlsx) umgebaut
- Statt .docx + LLM jetzt .xlsx mit openpyxl parsen - Felder direkt aus EINGABE-Sheet gemappt: Spieltitel, Test von, Fakten, Mechanismen, PRO/KONTRA, Tags, Wertung - Autor wird automatisch aus 'Test von'-Feld erkannt - Infobox-Tabelle aus Excel-Feldern generiert - Keine LLM-Kosten mehr, keine Latenz - Formular akzeptiert jetzt .xlsx
This commit is contained in:
+140
-81
@@ -1903,11 +1903,11 @@ textarea{{min-height:100px;resize:vertical}}
|
||||
<h1>Review Builder</h1>
|
||||
<p style="color:var(--muted)">Word-Datei + Bilder hochladen. Inhalte werden automatisch zugeordnet.</p>
|
||||
<form id="reviewForm" enctype="multipart/form-data">
|
||||
<div class="card"><h2>1. Word-Datei hochladen</h2>
|
||||
<div class="drop-zone" id="wordDrop">
|
||||
<div class="drop-zone-label"><strong>Word-Datei hier ablegen</strong> oder klicken (.docx)</div>
|
||||
<input type="file" id="wordInput" accept=".docx" style="display:none">
|
||||
</div><ul class="file-list" id="wordList"></ul></div>
|
||||
<div class="card"><h2>1. Excel-Datei hochladen</h2>
|
||||
<div class="drop-zone" id="excelDrop">
|
||||
<div class="drop-zone-label"><strong>Bewertungs-Excel hier ablegen</strong> oder klicken (.xlsx)</div>
|
||||
<input type="file" id="excelInput" accept=".xlsx" style="display:none">
|
||||
</div><ul class="file-list" id="excelList"></ul></div>
|
||||
<div class="card"><h2>2. Bilder hochladen</h2>
|
||||
<div class="drop-zone" id="imageDrop">
|
||||
<div class="drop-zone-label"><strong>Bilder hier ablegen</strong> oder klicken</div>
|
||||
@@ -1939,23 +1939,23 @@ textarea{{min-height:100px;resize:vertical}}
|
||||
</div>
|
||||
<div class="toast" id="toast"></div>
|
||||
<script>
|
||||
const wordDrop=document.getElementById('wordDrop'),wordInput=document.getElementById('wordInput'),wordList=document.getElementById('wordList');
|
||||
const excelDrop=document.getElementById('excelDrop'),excelInput=document.getElementById('excelInput'),excelList=document.getElementById('excelList');
|
||||
const imageDrop=document.getElementById('imageDrop'),imageInput=document.getElementById('imageInput'),imageList=document.getElementById('imageList');
|
||||
let wordFile=null,imageFiles=[];
|
||||
let excelFile=null,imageFiles=[];
|
||||
function st(msg,type){{var t=document.getElementById('toast');t.textContent=msg;t.className='toast '+type+' show';clearTimeout(t._t);t._t=setTimeout(function(){{t.classList.remove('show')}},3000)}}
|
||||
wordDrop.addEventListener('click',function(){{wordInput.click()}});
|
||||
wordDrop.addEventListener('dragover',function(e){{e.preventDefault();wordDrop.classList.add('dragover')}});
|
||||
wordDrop.addEventListener('dragleave',function(){{wordDrop.classList.remove('dragover')}});
|
||||
wordDrop.addEventListener('drop',function(e){{e.preventDefault();wordDrop.classList.remove('dragover');if(e.dataTransfer.files[0])awf(e.dataTransfer.files[0])}});
|
||||
wordInput.addEventListener('change',function(){{if(wordInput.files[0])awf(wordInput.files[0])}});
|
||||
function awf(f){{wordFile=f;wordList.innerHTML='<li>'+f.name+' ('+(f.size/1024).toFixed(1)+' KB) <span class="remove" onclick="wordFile=null;wordList.innerHTML=\\'\\';">X</span></li>'}}
|
||||
excelDrop.addEventListener('click',function(){{excelInput.click()}});
|
||||
excelDrop.addEventListener('dragover',function(e){{e.preventDefault();excelDrop.classList.add('dragover')}});
|
||||
excelDrop.addEventListener('dragleave',function(){{excelDrop.classList.remove('dragover')}});
|
||||
excelDrop.addEventListener('drop',function(e){{e.preventDefault();excelDrop.classList.remove('dragover');if(e.dataTransfer.files[0])aef(e.dataTransfer.files[0])}});
|
||||
excelInput.addEventListener('change',function(){{if(excelInput.files[0])aef(excelInput.files[0])}});
|
||||
function aef(f){{excelFile=f;excelList.innerHTML='<li>'+f.name+' ('+(f.size/1024).toFixed(1)+' KB) <span class="remove" onclick="excelFile=null;excelList.innerHTML=\\'\\';">X</span></li>'}}
|
||||
imageDrop.addEventListener('click',function(){{imageInput.click()}});
|
||||
imageDrop.addEventListener('dragover',function(e){{e.preventDefault();imageDrop.classList.add('dragover')}});
|
||||
imageDrop.addEventListener('dragleave',function(){{imageDrop.classList.remove('dragover')}});
|
||||
imageDrop.addEventListener('drop',function(e){{e.preventDefault();imageDrop.classList.remove('dragover');aif(e.dataTransfer.files)}});
|
||||
imageInput.addEventListener('change',function(){{aif(imageInput.files)}});
|
||||
function aif(files){{for(var i=0;i<files.length;i++){{(function(f){{imageFiles.push(f);var li=document.createElement('li');li.innerHTML=f.name+' ('+(f.size/1024).toFixed(1)+' KB) <span class="remove">X</span>';li.querySelector('.remove').onclick=function(){{imageFiles=imageFiles.filter(function(x){{return x!==f}});li.remove()}};imageList.appendChild(li)}})(files[i])}}}}
|
||||
async function submitForm(publish){{var s=document.getElementById('spielname').value.trim();if(!s){{st('Spielname fehlt','err');return}}if(!wordFile){{st('Word-Datei fehlt','err');return}}var fd=new FormData();fd.append('word',wordFile);imageFiles.forEach(function(f){{fd.append('images',f)}});fd.append('spielname',s);fd.append('autor',document.getElementById('autor').value);fd.append('rubrik',document.getElementById('rubrik').value);fd.append('komplexitaet',document.getElementById('komplexitaet').value);fd.append('disclosure',document.getElementById('disclosure').value);fd.append('verlag',document.getElementById('verlag').value.trim());fd.append('fantasywelt_url',document.getElementById('fantasywelt_url').value.trim());fd.append('so_url',document.getElementById('so_url').value.trim());fd.append('publish',publish?'1':'0');st(publish?'Veroffentliche...':'Baue Vorschau...','info');try{{var resp=await fetch('/review-builder/build?key={access_token}',{{method:'POST',body:fd}});var data=await resp.json();if(!data.ok){{st(data.error||'Fehler','err');return}}var box=document.getElementById('resultBox');box.classList.add('visible');document.getElementById('resultTitle').textContent=data.title;document.getElementById('resultLink').innerHTML=data.published?'<a href="'+data.article_url+'" target="_blank">Artikel online: '+data.article_url+'</a>':'<a href="/'+data.filename+'?key={access_token}" target="_blank">Vorschau: '+data.filename+'</a>';st(data.published?'Veroffentlicht!':'Vorschau bereit','ok')}}catch(e){{st('Fehler: '+e.message,'err')}}}}
|
||||
async function submitForm(publish){{var s=document.getElementById('spielname').value.trim();if(!s){{st('Spielname fehlt','err');return}}if(!excelFile){{st('Excel-Datei fehlt','err');return}}var fd=new FormData();fd.append('excel',excelFile);imageFiles.forEach(function(f){{fd.append('images',f)}});fd.append('spielname',s);fd.append('autor',document.getElementById('autor').value);fd.append('rubrik',document.getElementById('rubrik').value);fd.append('komplexitaet',document.getElementById('komplexitaet').value);fd.append('disclosure',document.getElementById('disclosure').value);fd.append('verlag',document.getElementById('verlag').value.trim());fd.append('fantasywelt_url',document.getElementById('fantasywelt_url').value.trim());fd.append('so_url',document.getElementById('so_url').value.trim());fd.append('publish',publish?'1':'0');st(publish?'Veroffentliche...':'Baue Vorschau...','info');try{{var resp=await fetch('/review-builder/build?key={access_token}',{{method:'POST',body:fd}});var data=await resp.json();if(!data.ok){{st(data.error||'Fehler','err');return}}var box=document.getElementById('resultBox');box.classList.add('visible');document.getElementById('resultTitle').textContent=data.title;document.getElementById('resultLink').innerHTML=data.published?'<a href="'+data.article_url+'" target="_blank">Artikel online: '+data.article_url+'</a>':'<a href="/'+data.filename+'?key={access_token}" target="_blank">Vorschau: '+data.filename+'</a>';st(data.published?'Veroffentlicht!':'Vorschau bereit','ok')}}catch(e){{st('Fehler: '+e.message,'err')}}}}
|
||||
document.getElementById('previewBtn').addEventListener('click',function(){{submitForm(false)}});
|
||||
document.getElementById('publishBtn').addEventListener('click',function(){{submitForm(true)}});
|
||||
</script></body></html>"""
|
||||
@@ -1979,7 +1979,7 @@ document.getElementById('publishBtn').addEventListener('click',function(){{submi
|
||||
)
|
||||
|
||||
def _handle_review_builder_build(self):
|
||||
"""POST: Word parsen, LLM-strukturieren, HTML bauen."""
|
||||
"""POST: Excel parsen, HTML bauen."""
|
||||
ctype = self.headers.get("Content-Type", "")
|
||||
if "multipart/form-data" not in ctype:
|
||||
self._json_reply(400, {"ok": False, "error": "Nur multipart/form-data"})
|
||||
@@ -1990,68 +1990,91 @@ document.getElementById('publishBtn').addEventListener('click',function(){{submi
|
||||
if boundary.startswith('"') and boundary.endswith('"'):
|
||||
boundary = boundary[1:-1]
|
||||
parts = _parse_multipart(raw, boundary)
|
||||
word_data = parts.get("word")
|
||||
excel_data = parts.get("excel")
|
||||
image_datas = [v for k, v in parts.items() if k.startswith("images_")]
|
||||
meta = {k: v for k, v in parts.items() if k not in ("word",) and not k.startswith("images_")}
|
||||
if not word_data:
|
||||
self._json_reply(400, {"ok": False, "error": "Keine Word-Datei"})
|
||||
meta = {k: v for k, v in parts.items() if k not in ("excel",) and not k.startswith("images_")}
|
||||
if not excel_data:
|
||||
self._json_reply(400, {"ok": False, "error": "Keine Excel-Datei"})
|
||||
return
|
||||
try:
|
||||
from docx import Document
|
||||
doc = Document(io.BytesIO(word_data))
|
||||
word_text = "\n".join(p.text for p in doc.paragraphs if p.text.strip())
|
||||
import openpyxl
|
||||
wb = openpyxl.load_workbook(io.BytesIO(excel_data), data_only=True)
|
||||
ws = wb["EINGABE"]
|
||||
xl = {}
|
||||
for row in ws.iter_rows(min_row=1, values_only=True):
|
||||
key = str(row[0]).strip() if row[0] else ""
|
||||
val = str(row[1]).strip() if row[1] else ""
|
||||
if key and val:
|
||||
xl[key] = val
|
||||
except Exception as e:
|
||||
self._json_reply(400, {"ok": False, "error": f"Word-Fehler: {e}"})
|
||||
self._json_reply(400, {"ok": False, "error": f"Excel-Fehler: {e}"})
|
||||
return
|
||||
spielname = meta.get("spielname", "Unbekannt").strip()
|
||||
|
||||
spielname = xl.get("Spieltitel", meta.get("spielname", "Unbekannt")).strip()
|
||||
zusatz = xl.get("Zusatztitel", "").strip()
|
||||
tester = xl.get("Test von", "").strip()
|
||||
|
||||
# Autor-Mapping: "Jan Maguna" → autor_key
|
||||
autor_key = meta.get("autor", "basti").strip()
|
||||
cfg = REVIEW_CONFIG
|
||||
if tester and autor_key == "basti":
|
||||
for k, v in cfg.get("authors", {}).items():
|
||||
if v["name"].lower() == tester.lower():
|
||||
autor_key = k
|
||||
break
|
||||
author_info = cfg.get("authors", {}).get(autor_key, {"name": tester or autor_key, "banner": "sebastian_meine_meinung_uberschrift.png"})
|
||||
|
||||
rubrik_id = int(meta.get("rubrik", "8"))
|
||||
komplex = meta.get("komplexitaet", "").strip()
|
||||
disclosure_id = meta.get("disclosure", "verlag_gestellt").strip()
|
||||
verlag_name = meta.get("verlag", "").strip()
|
||||
do_publish = meta.get("publish", "0") == "1"
|
||||
fantasywelt_url = meta.get("fantasywelt_url", "").strip()
|
||||
so_url = meta.get("so_url", "").strip()
|
||||
do_publish = meta.get("publish", "0") == "1"
|
||||
cfg = REVIEW_CONFIG
|
||||
author_info = cfg.get("authors", {}).get(autor_key, {"name": autor_key, "banner": "sebastian_meine_meinung_uberschrift.png"})
|
||||
|
||||
# Excel-Felder
|
||||
komplex = xl.get("Komplexität des Spiels", "").strip()
|
||||
autoren = xl.get("Autor:innen", "").strip()
|
||||
illustratoren = xl.get("Illustrator:innen", "").strip()
|
||||
verlag = xl.get("Verlag/Vertrieb", "").strip()
|
||||
personen = xl.get("Personenzahl", "").strip()
|
||||
spielzeit = xl.get("Spielzeit", "").strip()
|
||||
jahr = xl.get("Erscheinungsjahr", "").strip()
|
||||
preis = xl.get("UVP/Ø-Preis", "").strip()
|
||||
app = xl.get("App-Unterstützung", "").strip()
|
||||
alter = xl.get("empfohlenes Alter", "").strip()
|
||||
wertung = xl.get("WERTUNG (1-100)", "").strip()
|
||||
sprache = xl.get("Sprache", "Deutsch").strip()
|
||||
|
||||
# Mechanismen
|
||||
mech = []
|
||||
for i in range(1, 5):
|
||||
m = xl.get(f"Mechanismus {i}", "").strip()
|
||||
if m: mech.append(m)
|
||||
|
||||
# Pro/Kontra
|
||||
pros = []
|
||||
for i in range(1, 5):
|
||||
p = xl.get(f"Pro {i}", "").strip()
|
||||
if p: pros.append(p)
|
||||
kontras = []
|
||||
for i in range(1, 5):
|
||||
k = xl.get(f"Kontra {i}", "").strip()
|
||||
if k: kontras.append(k)
|
||||
|
||||
# Tags
|
||||
tags = []
|
||||
for i in range(1, 11):
|
||||
t = xl.get(f"Tag {i}", "").strip()
|
||||
if t: tags.append(t)
|
||||
|
||||
# Disclosure aus Verlag
|
||||
verlag_name = meta.get("verlag", verlag).strip()
|
||||
disclosure_id = meta.get("disclosure", "verlag_gestellt").strip()
|
||||
disclosure_text = ""
|
||||
for d in cfg.get("disclosures", []):
|
||||
if d["id"] == disclosure_id:
|
||||
disclosure_text = d["text"].replace("{VERLAG}", verlag_name or "der Verlag").replace("{SPIEL}", spielname)
|
||||
break
|
||||
# LLM-Strukturierung
|
||||
teaser = word_text[:300]
|
||||
erklarung = word_text
|
||||
fazit = ""
|
||||
try:
|
||||
prompt = (f'Analysiere den Brettspiel-Review-Text fur "{spielname}". '
|
||||
f'Extrahiere:\n1. TEASER (1-3 einleitende Satze)\n'
|
||||
f'2. SPIELERKLARUNG (ausfuhrliche Spielbeschreibung)\n'
|
||||
f'3. FAZIT (personliche Meinung/Bewertung)\n\n'
|
||||
f'Ausgabeformat:\n---TEASER---\n[Text]\n---ERKLAERUNG---\n[Text]\n---FAZIT---\n[Text]\n\n'
|
||||
f'Text:\n{word_text[:8000]}')
|
||||
from urllib.request import Request, urlopen
|
||||
api_body = json.dumps({
|
||||
"model": "deepseek-v4-flash:cloud", "provider": "datenhimmel",
|
||||
"messages": [{"role": "system", "content": "Du bist ein praziser Textanalysator."},
|
||||
{"role": "user", "content": prompt}],
|
||||
"temperature": 0.1
|
||||
}).encode()
|
||||
req = Request("https://ui.datenhimmel.work/api/v1/chat/completions",
|
||||
data=api_body, headers={"Content-Type": "application/json",
|
||||
"Authorization": "Bearer sk-ce3...b6db"})
|
||||
resp = urlopen(req, timeout=30)
|
||||
result = json.loads(resp.read().decode())
|
||||
llm_text = result["choices"][0]["message"]["content"]
|
||||
tm = re.search(r'---TEASER---\s*(.*?)\s*---ERKLAERUNG---', llm_text, re.DOTALL)
|
||||
em = re.search(r'---ERKLAERUNG---\s*(.*?)\s*---FAZIT---', llm_text, re.DOTALL)
|
||||
fm = re.search(r'---FAZIT---\s*(.*)', llm_text, re.DOTALL)
|
||||
if tm: teaser = tm.group(1).strip()
|
||||
if em: erklarung = em.group(1).strip()
|
||||
if fm: fazit = fm.group(1).strip()
|
||||
except Exception as e:
|
||||
print(f"[review-builder] LLM fehlgeschlagen: {e}", flush=True)
|
||||
# Bilder speichern & resizen
|
||||
|
||||
# Bilder speichern
|
||||
erster = spielname[0].lower() if spielname else "x"
|
||||
ordner = spielname.lower().replace(" ", "")
|
||||
base_img = f"images/Bilderverzeichnis/01/{erster}/{ordner}"
|
||||
@@ -2062,53 +2085,89 @@ document.getElementById('publishBtn').addEventListener('click',function(){{submi
|
||||
_resize_image(img_data, os.path.join(wd, f"bild_{idx+1}.jpg"))
|
||||
except Exception as e:
|
||||
print(f"[review-builder] Bild {idx} fehlgeschlagen: {e}", flush=True)
|
||||
|
||||
# HTML bauen
|
||||
datum = datetime.now().strftime("%d. %B %Y")
|
||||
autor_name = author_info["name"]
|
||||
autor_banner = author_info["banner"]
|
||||
if not teaser.startswith("<"): teaser = f"<p>{teaser}</p>"
|
||||
if not erklarung.startswith("<"): erklarung = f"<p>{erklarung.replace(chr(10)+chr(10), '</p><p>')}</p>"
|
||||
if fazit and not fazit.startswith("<"): fazit = f"<p>{fazit.replace(chr(10)+chr(10), '</p><p>')}</p>"
|
||||
|
||||
# Infobox-Tabelle
|
||||
fakten_rows = []
|
||||
if autoren: fakten_rows.append(f'<tr><td><strong>Autor:innen</strong></td><td>{autoren}</td></tr>')
|
||||
if illustratoren: fakten_rows.append(f'<tr><td><strong>Illustrator:innen</strong></td><td>{illustratoren}</td></tr>')
|
||||
if verlag: fakten_rows.append(f'<tr><td><strong>Verlag</strong></td><td>{verlag}</td></tr>')
|
||||
if personen: fakten_rows.append(f'<tr><td><strong>Personenzahl</strong></td><td>{personen}</td></tr>')
|
||||
if spielzeit: fakten_rows.append(f'<tr><td><strong>Spielzeit</strong></td><td>{spielzeit}</td></tr>')
|
||||
if jahr: fakten_rows.append(f'<tr><td><strong>Erscheinungsjahr</strong></td><td>{jahr}</td></tr>')
|
||||
if preis: fakten_rows.append(f'<tr><td><strong>Preis</strong></td><td>{preis}</td></tr>')
|
||||
if app: fakten_rows.append(f'<tr><td><strong>App</strong></td><td>{app}</td></tr>')
|
||||
if alter: fakten_rows.append(f'<tr><td><strong>Alter</strong></td><td>{alter}</td></tr>')
|
||||
fakten_html = ""
|
||||
if fakten_rows:
|
||||
fakten_html = '<table class="bsn-table"><tbody>' + "".join(fakten_rows) + '</tbody></table>'
|
||||
|
||||
# Pro/Kontra
|
||||
pro_html = ""
|
||||
if pros:
|
||||
pro_html = '<div><h3>👍 PRO</h3><ul>' + "".join(f"<li>{p}</li>" for p in pros) + '</ul></div>'
|
||||
kontra_html = ""
|
||||
if kontras:
|
||||
kontra_html = '<div><h3>👎 KONTRA</h3><ul>' + "".join(f"<li>{k}</li>" for k in kontras) + '</ul></div>'
|
||||
|
||||
# Mechanismen
|
||||
mech_html = ""
|
||||
if mech:
|
||||
mech_html = '<p><strong>Mechanismen:</strong> ' + ", ".join(mech) + '</p>'
|
||||
|
||||
# Bewertung
|
||||
wertung_html = ""
|
||||
if wertung:
|
||||
wertung_html = f'<div class="bsn-rating"><span class="bsn-rating-score">{wertung}</span><span class="bsn-rating-label">/ 100</span></div>'
|
||||
|
||||
disc_html = ""
|
||||
if disclosure_text:
|
||||
disc_html = f'<p style="text-align:center;"><em>{disclosure_text}<br>Dies hat keinen Einfluss auf unsere Bewertung!</em></p>'
|
||||
|
||||
aff_html = ""
|
||||
if fantasywelt_url:
|
||||
aff_html += f'<p><a href="{fantasywelt_url}" target="_blank" rel="noopener"><img style="margin:10px auto;display:block;" src="images/banners/fantasywelt/banner_fantasywelt-jetzt-kaufen.jpg" alt="Jetzt bei Fantasywelt kaufen"></a></p>\n'
|
||||
aff_html += f'<p><a href="{fantasywelt_url}" target="_blank" rel="noopener"><img style="margin:10px auto;display:block;" src="images/banners/fantasywelt/banner_fantasywelt-jetzt-kaufen.jpg" alt="Bei Fantasywelt kaufen"></a></p>\n'
|
||||
if so_url:
|
||||
aff_html += f'<p><a href="{so_url}" target="_blank" rel="noopener"><img style="margin:10px auto;display:block;" src="images/banners/fantasywelt/SO_kaufen_button.jpg" alt="Bei Spiele-Offensive kaufen"></a></p>\n'
|
||||
|
||||
gal_path = f"01/{erster}/{ordner}"
|
||||
body = f"""{teaser}
|
||||
tags_str = ", ".join(tags) if tags else f"{spielname}, Brettspiel, Review, Bewertung, {autor_name}"
|
||||
title_full = f"{spielname} {zusatz}".strip()
|
||||
|
||||
body = f"""<p>{spielname} — {komplex or 'Brettspiel'} von {autoren or verlag or 'unbekannt'}</p>
|
||||
<hr id="system-readmore">
|
||||
<p> </p>
|
||||
<p><img style="margin:10px auto;display:block;" src="{base_img}/wertung/info.png" alt="Fakten zum Spiel"></p>
|
||||
{fakten_html}
|
||||
{disc_html}
|
||||
<h2>So spielt sich {spielname}</h2>
|
||||
{erklarung}
|
||||
<hr class="system-pagebreak" title="Fazit + Wertung + Bilder vom Spiel">
|
||||
{mech_html}
|
||||
{pro_html}
|
||||
{kontra_html}
|
||||
<h2><img src="images/banners/redaktion/{autor_banner}" alt="Meine Meinung"></h2>
|
||||
{fazit}
|
||||
{wertung_html}
|
||||
<p> </p>
|
||||
<p><img style="margin:10px auto;display:block;" src="{base_img}/wertung/wertung.png" alt="Wertung zum Spiel"></p>
|
||||
<p> </p>
|
||||
<p style="text-align:center;">{{module Discord-Hinweis Tests}}</p>
|
||||
<p style="text-align:center;">{{{{module Discord-Hinweis Tests}}}}</p>
|
||||
<p> </p>
|
||||
{aff_html}
|
||||
<p> </p>
|
||||
<h2>Bilder vom Spiel</h2>
|
||||
<p>{{gallery}}{gal_path}{{/gallery}}</p>
|
||||
<p>{{module Aktuelle Texte fur Content}}</p>"""
|
||||
<p>{{{{gallery}}}}{gal_path}{{{{/gallery}}}}</p>
|
||||
<p>{{{{module Aktuelle Texte fur Content}}}}</p>"""
|
||||
|
||||
datei = datetime.now().strftime("%Y-%m-%d")
|
||||
safe = spielname.lower().replace(" ", "-").replace(":", "").replace("/", "-")
|
||||
filename = f"artikel_{safe}_{datei}.html"
|
||||
filepath = os.path.join(WORKSPACE, filename)
|
||||
desc = teaser.replace("<p>", "").replace("</p>", "")[:150]
|
||||
full = f"""<!DOCTYPE html>
|
||||
<html lang="de"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta name="description" content="Brettspiel-Review: {spielname} \u2014 {desc}">
|
||||
<meta name="keywords" content="{spielname},Brettspiel,Review,Bewertung,{autor_name}">
|
||||
<meta name="description" content="Brettspiel-Review: {title_full}">
|
||||
<meta name="keywords" content="{tags_str}">
|
||||
<meta name="category-id" content="{rubrik_id}">
|
||||
<title>{spielname} \u2014 Review von {autor_name} | Brettspiel-News.de</title></head>
|
||||
<title>{title_full} — Review von {autor_name} | Brettspiel-News.de</title></head>
|
||||
<body><article><p class="meta">{datum} | von {autor_name}</p>{body}</article></body></html>"""
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
f.write(full)
|
||||
|
||||
Reference in New Issue
Block a user