Kühlfinder MVP: Finder, Geräte, Karte (MapLibre), Preis-Intelligenz + Wucher-Warnung, PortaSplit-Hub, llms.txt
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import { allProducts } from '../lib/pricing';
|
||||
---
|
||||
<Base title="Verfügbarkeits-Alarm für Klimageräte | Kühlfinder" description="Lass dich benachrichtigen, sobald dein Wunsch-Klimagerät in deiner Nähe verfügbar ist.">
|
||||
<section class="wrap" style="margin-top:1.6rem;max-width:640px">
|
||||
<p class="kicker"><span class="u"></span>Verfügbarkeits-Alarm</p>
|
||||
<h1>Sag mir Bescheid, sobald es verfügbar ist</h1>
|
||||
<p class="muted">Kostenloser E-Mail-Alarm. Wir melden uns, wenn ein passendes Gerät in deinem Umkreis auftaucht. Keine Garantie auf Preis oder Bestand.</p>
|
||||
<form class="card" id="alarmform" style="display:grid;gap:.9rem">
|
||||
<label>Gerät<select name="produkt">
|
||||
<option value="">Alle / egal</option>
|
||||
{allProducts.map(p=> <option value={p.slug}>{p.name}</option>)}
|
||||
</select></label>
|
||||
<label>PLZ<input name="plz" inputmode="numeric" pattern="[0-9]{5}" placeholder="z. B. 21255" required /></label>
|
||||
<label>Umkreis<select name="radius"><option>25 km</option><option selected>50 km</option><option>100 km</option></select></label>
|
||||
<label>E-Mail<input name="email" type="email" placeholder="du@example.de" required /></label>
|
||||
<label style="font-weight:400;font-size:.9rem;display:flex;gap:.5rem"><input type="checkbox" required /> Ich akzeptiere die <a href="/datenschutz">Datenschutzhinweise</a>.</label>
|
||||
<button class="btn" type="submit">Alarm aktivieren</button>
|
||||
<p class="note" id="alarmmsg"></p>
|
||||
</form>
|
||||
<p class="note" style="margin-top:1rem">Später: Telegram/Push und ein Sommer-Pass für engmaschige Alerts. Heute startet der Alarm als kostenloser Lead-Magnet.</p>
|
||||
</section>
|
||||
</Base>
|
||||
<script>
|
||||
const f=document.getElementById('alarmform');
|
||||
f.addEventListener('submit',e=>{e.preventDefault();document.getElementById('alarmmsg').textContent='Demo: Alarm-Backend folgt (Listmonk/Telegram). Eingaben wurden nicht gespeichert.';});
|
||||
</script>
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
---
|
||||
<Base title="Datenschutz | Kühlfinder">
|
||||
<section class="wrap prose" style="margin-top:1.6rem;max-width:720px">
|
||||
<h1>Datenschutz</h1>
|
||||
<p><em>Platzhalter, vor Go-Live finalisieren.</em></p>
|
||||
<p>Diese Seite wird statisch ausgeliefert. Kartenkacheln werden von OpenFreeMap geladen; dabei wird deine IP an den Tile-Server übertragen (vor Go-Live ggf. self-hosten). Standortabfrage erfolgt nur nach deiner ausdrücklichen Zustimmung im Browser. Der Verfügbarkeits-Alarm speichert E-Mail und PLZ ausschließlich zur Benachrichtigung (Backend folgt, EU-Hosting).</p>
|
||||
</section>
|
||||
</Base>
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import { allProducts } from '../lib/pricing';
|
||||
const products = allProducts.map(p=>({slug:p.slug,name:p.name,type:p.type,badge:p.badge,uvp:p.uvp,apartment:p.apartmentSuitable}));
|
||||
---
|
||||
<Base title="Klima-Finder: welche mobile Klimaanlage passt zu dir? | Kühlfinder" description="Beantworte wenige Fragen und finde heraus, ob mobile Split oder Monoblock passt – mit Verfügbarkeits-Check.">
|
||||
<section class="wrap" style="margin-top:1.6rem;max-width:720px">
|
||||
<p class="kicker"><span class="u"></span>60-Sekunden-Finder</p>
|
||||
<h1>Welches portable Klimagerät passt zu dir?</h1>
|
||||
<div id="finder" class="card"></div>
|
||||
</section>
|
||||
</Base>
|
||||
<script define:vars={{ products }}>
|
||||
const Q = [
|
||||
{k:'room',q:'Welcher Raum?',a:['Schlafzimmer','Homeoffice','Wohnzimmer','Dachgeschoss','Kinderzimmer']},
|
||||
{k:'size',q:'Wie groß ist der Raum?',a:['bis 15 m²','15–25 m²','25–40 m²','über 40 m²']},
|
||||
{k:'outdoor',q:'Außengerät möglich (Balkon/Fensterbank)?',a:['Ja','Nein','Unsicher']},
|
||||
{k:'window',q:'Welche Fensterart?',a:['Dreh-Kipp','Balkontür','Dachfenster','Schiebefenster','keine gute Möglichkeit']},
|
||||
{k:'noise',q:'Wie wichtig ist Leise?',a:['Sehr','Mittel','Egal']},
|
||||
{k:'budget',q:'Budget?',a:['unter 500 €','500–800 €','800–1300 €','über 1300 €']},
|
||||
{k:'urgency',q:'Wie dringend?',a:['Heute','Diese Woche','Nur informieren']}
|
||||
];
|
||||
const ans={}; let i=0;
|
||||
const el=document.getElementById('finder');
|
||||
const find=s=>products.find(p=>p.slug===s);
|
||||
function pick(){
|
||||
const out=find('midea-portasplit-12000'); let primary, alt, budget, warn='';
|
||||
const outdoor=ans.outdoor==='Ja';
|
||||
const noOutlet=ans.window==='keine gute Möglichkeit';
|
||||
const hiBudget=(ans.budget==='800–1300 €'||ans.budget==='über 1300 €');
|
||||
const smallRoom=(ans.size==='bis 15 m²'||ans.size==='15–25 m²');
|
||||
if(noOutlet){warn='Ohne Abluft-/Außenmöglichkeit kann kein echtes Klimagerät die Wärme abführen. Prüfe Dreh-Kipp-Fenster oder Balkontür – sonst hilft nur Verschattung und Lüften.';}
|
||||
if(ans.room==='Schlafzimmer'&&outdoor&&smallRoom) primary='midea-portasplit-cool-8000';
|
||||
else if(outdoor&&hiBudget) primary='midea-portasplit-12000';
|
||||
else if(!outdoor&&hiBudget) primary='delonghi-pac-ex105';
|
||||
else if(!outdoor&&ans.budget==='500–800 €') primary='midea-silent-cool-26-pro-wf';
|
||||
else if(ans.budget==='unter 500 €') primary='comfee-mobile-12000';
|
||||
else primary='delonghi-pac-ex105';
|
||||
alt = (find(primary).type==='mobile_split')?'delonghi-pac-ex105':'midea-portasplit-cool-8000';
|
||||
budget = 'comfee-mobile-12000';
|
||||
if(find(primary).type==='monoblock') warn = warn || 'Tipp: Monoblock braucht eine gute Fensterabdichtung, sonst sinkt die Kühlleistung.';
|
||||
return {primary:find(primary),alt:find(alt),budget:find(budget),warn};
|
||||
}
|
||||
function result(){
|
||||
const r=pick();
|
||||
const card=p=>`<a class="rc" href="/geraete/${p.slug}"><strong>${p.name}</strong><span class="note">${p.badge}</span></a>`;
|
||||
el.innerHTML=`<h2>Deine Empfehlung</h2>
|
||||
<div class="reco"><span class="badge">Beste Wahl</span>${card(r.primary)}</div>
|
||||
<div class="reco"><span class="badge">Wenn ausverkauft</span>${card(r.alt)}</div>
|
||||
<div class="reco"><span class="badge">Budget</span>${card(r.budget)}</div>
|
||||
${r.warn?`<p class="warn">${r.warn}</p>`:''}
|
||||
<div class="rcta">
|
||||
<a class="btn" href="/karte?produkt=${r.primary.slug}">Verfügbarkeit auf Karte</a>
|
||||
<a class="btn ghost" href="/alarm?produkt=${r.primary.slug}">Alarm setzen</a>
|
||||
<button class="btn ghost" id="again">Nochmal</button>
|
||||
</div>`;
|
||||
document.getElementById('again').onclick=()=>{i=0;step();};
|
||||
}
|
||||
function step(){
|
||||
if(i>=Q.length) return result();
|
||||
const q=Q[i];
|
||||
el.innerHTML=`<div class="prog">Frage ${i+1} / ${Q.length}</div><h2>${q.q}</h2>
|
||||
<div class="opts">${q.a.map(a=>`<button class="opt" data-v="${a}">${a}</button>`).join('')}</div>`;
|
||||
el.querySelectorAll('.opt').forEach(b=>b.onclick=()=>{ans[q.k]=b.dataset.v;i++;step();});
|
||||
}
|
||||
step();
|
||||
</script>
|
||||
<style is:global>
|
||||
#finder .prog{font-size:.85rem;color:var(--slate);margin-bottom:.3rem}
|
||||
#finder .opts{display:grid;gap:.6rem;margin-top:1rem}
|
||||
#finder .opt{font-family:var(--font-body);font-size:1rem;text-align:left;padding:.9rem 1.1rem;border:1px solid var(--line);border-radius:var(--radius);background:var(--card);cursor:pointer;transition:border-color .15s,background .15s}
|
||||
#finder .opt:hover{border-color:var(--petrol);background:var(--petrol-soft)}
|
||||
#finder .reco{display:flex;align-items:center;gap:.8rem;margin:.6rem 0}
|
||||
#finder .rc{display:flex;flex-direction:column;text-decoration:none;color:var(--ink);flex:1;border:1px solid var(--line);border-radius:var(--radius);padding:.7rem .9rem}
|
||||
#finder .rc:hover{border-color:var(--petrol)}
|
||||
#finder .warn{background:var(--warm-bg);color:oklch(0.42 0.11 47);padding:.8rem 1rem;border-radius:var(--radius);margin-top:.8rem}
|
||||
#finder .rcta{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:1rem}
|
||||
</style>
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import { allProducts, getProduct, priceIntel, offersFor, storeById, euro } from '../../lib/pricing';
|
||||
export function getStaticPaths(){ return allProducts.map(p => ({ params: { slug: p.slug } })); }
|
||||
const { slug } = Astro.params;
|
||||
const product = getProduct(slug!)!;
|
||||
const pi = priceIntel(product);
|
||||
const typeLabel = product.type === 'mobile_split' ? 'Mobile Split-Klimaanlage' : 'Monoblock mit Abluftschlauch';
|
||||
const availStores = offersFor(product.id)
|
||||
.filter(o => o.sourceType==='store' && ['in_stock','low_stock'].includes(o.availability))
|
||||
.map(o => ({...o, store: storeById(o.sourceId)}));
|
||||
const alternatives = allProducts.filter(p => p.id!==product.id && p.type===product.type).slice(0,3);
|
||||
const jsonld = {
|
||||
'@context':'https://schema.org','@type':'Product',name:product.name,brand:product.brand,
|
||||
category:'Mobile Klimaanlage',
|
||||
offers:{'@type':'AggregateOffer',priceCurrency:'EUR',lowPrice:pi.lowestLegit ?? product.uvp,highPrice:product.uvp,offerCount:offersFor(product.id).length}
|
||||
};
|
||||
---
|
||||
<Base title={`${product.name} – Test, Eignung & Verfügbarkeit | Kühlfinder`} description={product.shortVerdict} jsonld={jsonld}>
|
||||
<section class="wrap" style="margin-top:1.4rem">
|
||||
<p class="note"><a href="/geraete">← Alle Geräte</a></p>
|
||||
<span class="badge">{product.badge}</span>
|
||||
{product.apartmentSuitable && <span class="badge tag-ok">wohnungstauglich</span>}
|
||||
<h1>{product.name}</h1>
|
||||
<p class="muted">{typeLabel} · {product.btuh.toLocaleString('de-DE')} BTU · {product.coolingKw} kW · {product.roomSizeM2[0]}–{product.roomSizeM2[1]} m² · EEK {product.efficiencyCooling}</p>
|
||||
<p class="lead">{product.shortVerdict}</p>
|
||||
|
||||
<div class="price-box card">
|
||||
<div class="status-grid">
|
||||
<div><span class="note">Üblicher Preis</span><br /><strong>{euro(pi.fairPrice)}</strong></div>
|
||||
<div><span class="note">Seriös aktuell ab</span><br /><strong>{euro(pi.lowestLegit)}</strong></div>
|
||||
<div><span class="note">Ø seriös</span><br /><strong>{euro(pi.avgLegit)}</strong></div>
|
||||
<div><span class="note">Märkte mit Bestand</span><br /><strong>{pi.storesAvailable}</strong></div>
|
||||
</div>
|
||||
{pi.wucher && <p class="wucher">⚠ <strong>Wucher-Warnung:</strong> bis {euro(pi.wucher.price)} bei Drittanbietern – über {Math.round((pi.wucher.price/pi.fairPrice-1)*100)} % über üblich. Nicht überteuert kaufen.</p>}
|
||||
<div class="status-cta">
|
||||
<a class="btn sm" href={`/karte?produkt=${product.slug}`}>Auf der Karte suchen</a>
|
||||
<a class="btn sm ghost" href={`/alarm?produkt=${product.slug}`}>Alarm setzen</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{product.type==='monoblock' && <p class="hint">Wichtig: Monoblock-Geräte brauchen eine gute Fensterabdichtung. Ohne Abdichtung wird ständig warme Luft nachgezogen – das kostet Leistung und Strom.</p>}
|
||||
{product.type==='mobile_split' && <p class="hint">Wichtig: Das Außengerät muss sicher und rücksichtsvoll platziert werden. Prüfe Fensterbank/Balkon, Nachbarn und Mietvertrag.</p>}
|
||||
|
||||
<div class="cols">
|
||||
<div class="card">
|
||||
<h3>Geeignet für</h3>
|
||||
<ul>{product.bestFor.map(x => <li>{x}</li>)}</ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Nicht geeignet, wenn</h3>
|
||||
<ul>{product.notFor.map(x => <li>{x}</li>)}</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cols">
|
||||
<div class="card"><h3>Stärken</h3><ul>{product.pros.map(x => <li>{x}</li>)}</ul></div>
|
||||
<div class="card"><h3>Schwächen</h3><ul>{product.cons.map(x => <li>{x}</li>)}</ul></div>
|
||||
</div>
|
||||
|
||||
{availStores.length>0 && (
|
||||
<div style="margin-top:1.4rem">
|
||||
<h2>Aktuell verfügbar in</h2>
|
||||
<div class="grid stores">
|
||||
{availStores.map(o => (
|
||||
<div class="card srow">
|
||||
<div><strong>{o.chain}</strong> {o.store?.city}<br /><span class="note">{o.store?.name}</span></div>
|
||||
<div style="text-align:right">{euro(o.price)}<br /><span class={o.availability==='low_stock'?'badge tag-warn':'badge tag-ok'}>{o.availability==='low_stock'?'wenig Bestand':'verfügbar'}</span></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h2 style="margin-top:1.6rem">Alternativen</h2>
|
||||
<div class="grid stores">
|
||||
{alternatives.map(a => <a class="card altlink" href={`/geraete/${a.slug}`}><strong>{a.name}</strong><br /><span class="note">{a.badge}</span></a>)}
|
||||
</div>
|
||||
|
||||
<h2 style="margin-top:1.6rem">Quellen</h2>
|
||||
<ul>{product.sources.map(s => <li><a href={s.url} rel="nofollow">{s.label}</a></li>)}</ul>
|
||||
</section>
|
||||
</Base>
|
||||
<style>
|
||||
.lead{font-size:1.1rem;color:var(--slate);max-width:46rem}
|
||||
.price-box{margin:1rem 0}
|
||||
.status-grid{display:flex;gap:2rem;flex-wrap:wrap;margin-bottom:.6rem}
|
||||
.status-grid strong{font-size:1.25rem}
|
||||
.wucher{background:var(--danger-bg);color:var(--danger);padding:.7rem .9rem;border-radius:var(--radius);font-size:.92rem}
|
||||
.status-cta{display:flex;gap:.5rem;flex-wrap:wrap}
|
||||
.hint{background:var(--warm-bg);color:oklch(0.42 0.11 47);padding:.8rem 1rem;border-radius:var(--radius);font-size:.95rem}
|
||||
.cols{display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-top:1rem}
|
||||
.stores{grid-template-columns:repeat(2,1fr)}
|
||||
.srow{display:flex;justify-content:space-between;align-items:center}
|
||||
.altlink{text-decoration:none;color:var(--ink)}
|
||||
.altlink:hover{border-color:var(--petrol)}
|
||||
ul{margin:.2rem 0;padding-left:1.1rem}
|
||||
@media(max-width:820px){.cols,.stores{grid-template-columns:1fr}}
|
||||
</style>
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import ProductCard from '../../components/ProductCard.astro';
|
||||
import { allProducts } from '../../lib/pricing';
|
||||
const sorted = [...allProducts].sort((a,b)=>b.score-a.score);
|
||||
---
|
||||
<Base title="Alle portablen Klimageräte im Vergleich – Kühlfinder" description="Kuratierte mobile Split- und Monoblock-Klimageräte im Vergleich, mit Live-Verfügbarkeit und Preis-Check.">
|
||||
<section class="wrap" style="margin-top:2rem">
|
||||
<p class="kicker"><span class="u"></span>Kuratierter Vergleich</p>
|
||||
<h1>Alle Geräte</h1>
|
||||
<p class="muted">Nur echte Kompressorgeräte – mobile Split ohne feste Installation und Monoblock mit Abluftschlauch. Keine Verdunstungs- oder USB-Kühler.</p>
|
||||
<div class="filters" id="filters">
|
||||
<button class="chip on" data-f="all">Alle</button>
|
||||
<button class="chip" data-f="mobile_split">Mobile Split</button>
|
||||
<button class="chip" data-f="monoblock">Monoblock</button>
|
||||
<button class="chip" data-f="apartment">Wohnungstauglich</button>
|
||||
</div>
|
||||
<div class="grid cards3" id="list">
|
||||
{sorted.map(p => (
|
||||
<div class="item" data-type={p.type} data-apartment={String(p.apartmentSuitable)}>
|
||||
<ProductCard product={p} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</Base>
|
||||
<style>
|
||||
.filters{display:flex;gap:.5rem;flex-wrap:wrap;margin:1rem 0 1.4rem}
|
||||
.chip{font-family:var(--font-body);font-weight:500;border:1px solid var(--line);background:var(--card);color:var(--ink);padding:.45rem .9rem;border-radius:999px;cursor:pointer}
|
||||
.chip.on{background:var(--petrol);color:#fff;border-color:var(--petrol)}
|
||||
.cards3{grid-template-columns:repeat(3,1fr)}
|
||||
@media(max-width:820px){.cards3{grid-template-columns:1fr}}
|
||||
</style>
|
||||
<script>
|
||||
const chips = document.querySelectorAll('#filters .chip');
|
||||
const items = document.querySelectorAll('#list .item');
|
||||
chips.forEach(c => c.addEventListener('click', () => {
|
||||
chips.forEach(x => x.classList.remove('on'));
|
||||
c.classList.add('on');
|
||||
const f = c.getAttribute('data-f');
|
||||
items.forEach(it => {
|
||||
const el = it as HTMLElement;
|
||||
let show = true;
|
||||
if (f === 'mobile_split' || f === 'monoblock') show = el.dataset.type === f;
|
||||
else if (f === 'apartment') show = el.dataset.apartment === 'true';
|
||||
el.style.display = show ? '' : 'none';
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
---
|
||||
<Base title="Hitzeschutz & Klimawandel: effizient kühlen | Kühlfinder" description="Warum kühle Räume wichtiger werden – und wie man effizient und verantwortungsvoll kühlt.">
|
||||
<section class="wrap prose" style="margin-top:1.6rem;max-width:720px">
|
||||
<p class="kicker"><span class="u"></span>Kontext mit Quellen</p>
|
||||
<h1>Hitzeschutz ist kein Luxus mehr</h1>
|
||||
<p>Wenn Wohnungen über Tage aufheizen und nachts nicht abkühlen, belastet das Schlaf, Konzentration und Gesundheit – besonders Kinder, ältere und vorerkrankte Menschen.</p>
|
||||
<h2>Was der Klimawandel damit zu tun hat</h2>
|
||||
<p>Das Umweltbundesamt betont, dass hohe sommerliche Temperaturen ein hohes gesundheitliches Schädigungspotenzial haben und mehrtägige Hitzephasen besonders relevant sind. Laut DWD ist Deutschland bereits rund 1,8 K wärmer als 1961–1990. Aktuelle Attributionsstudien zeigen: Hitzewellen wie aktuell wären ohne menschengemachten Klimawandel praktisch unmöglich.</p>
|
||||
<h2>Effizient kühlen heißt nicht sinnlos frieren</h2>
|
||||
<ul>
|
||||
<li>Tagsüber verschatten, Fenster/Türen geschlossen halten.</li>
|
||||
<li>Nachts/früh lüften, wenn es abkühlt.</li>
|
||||
<li>Monoblock nur mit guter Fensterabdichtung.</li>
|
||||
<li>Nur genutzte Räume kühlen, Zieltemperatur moderat (24–26 °C).</li>
|
||||
<li>Stromverbrauch mit smarter Steckdose beobachten.</li>
|
||||
</ul>
|
||||
<p class="note">Quellen: Umweltbundesamt (Gesundheitsrisiken durch Hitze), DWD Klimastatusbericht 2025, Copernicus, World Weather Attribution.</p>
|
||||
</section>
|
||||
</Base>
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
---
|
||||
<Base title="Impressum | Kühlfinder">
|
||||
<section class="wrap prose" style="margin-top:1.6rem;max-width:720px">
|
||||
<h1>Impressum</h1>
|
||||
<p>Angaben gemäß § 5 TMG / § 18 MStV — <em>Platzhalter, vor Go-Live ausfüllen.</em></p>
|
||||
<p>Heidrich Digital<br />[Straße]<br />[PLZ Ort]<br />E-Mail: mail@tillheidrich.de</p>
|
||||
<p>Verantwortlich i. S. d. § 18 Abs. 2 MStV: Till Heidrich</p>
|
||||
</section>
|
||||
</Base>
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import ProductCard from '../components/ProductCard.astro';
|
||||
import { allProducts, priceIntel, euro, getProduct, dataUpdatedAt } from '../lib/pricing';
|
||||
const top = allProducts.filter(p => p.tier === 'A');
|
||||
const ps = getProduct('midea-portasplit-12000')!;
|
||||
const psi = priceIntel(ps);
|
||||
const updated = new Date(dataUpdatedAt).toLocaleString('de-DE',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'});
|
||||
const jsonld = {
|
||||
'@context':'https://schema.org','@type':'WebSite',name:'Kühlfinder',url:'https://kuehlfinder.de',
|
||||
description:'Portable Klimageräte finden und Live-Verfügbarkeit in Baumärkten sehen.'
|
||||
};
|
||||
---
|
||||
<Base title="Kühlfinder – gutes portables Klimagerät finden & Verfügbarkeit sehen" jsonld={jsonld}>
|
||||
<section class="hero"><div class="wrap">
|
||||
<p class="kicker"><span class="u"></span>Ehrlicher Klima-Finder für Mietwohnungen</p>
|
||||
<h1>Finde ein gutes portables Klimagerät – und sieh, wo es heute verfügbar ist.</h1>
|
||||
<p class="lead">Nur echte Kompressorgeräte, keine Fake-Kühler. Mit Live-Verfügbarkeit in Baumärkten, Preis-Check gegen Wucher und Alarm, sobald dein Gerät wieder da ist.</p>
|
||||
<div class="modes">
|
||||
<a class="mode" href="/finder"><strong>Ich filtere</strong><span>Welches Gerät passt zu meinem Raum?</span></a>
|
||||
<a class="mode" href="/karte"><strong>Ich sehe die Karte</strong><span>Alle Märkte mit aktuellem Bestand</span></a>
|
||||
<a class="mode" href="/alarm"><strong>Ich lasse mich benachrichtigen</strong><span>Alarm bei Verfügbarkeit in der Nähe</span></a>
|
||||
</div>
|
||||
</div></section>
|
||||
|
||||
<section class="wrap"><div class="status card">
|
||||
<div class="status-head">
|
||||
<h2 style="margin:0">Midea PortaSplit – aktueller Status</h2>
|
||||
<span class="note">Stand: {updated}</span>
|
||||
</div>
|
||||
<div class="status-grid">
|
||||
<div><span class="note">Üblicher Preis (UVP)</span><br /><strong>{euro(psi.fairPrice)}</strong></div>
|
||||
<div><span class="note">Seriös aktuell ab</span><br /><strong>{euro(psi.lowestLegit)}</strong></div>
|
||||
<div><span class="note">Märkte mit Bestand</span><br /><strong>{psi.storesAvailable}</strong></div>
|
||||
</div>
|
||||
{psi.wucher && (
|
||||
<p class="wucher">⚠ <strong>Wucher-Warnung:</strong> Einzelne Drittanbieter verlangen aktuell bis {euro(psi.wucher.price)} – das ist deutlich über dem üblichen Preis von {euro(psi.fairPrice)}. Nicht in Panik überteuert kaufen. Besser: Alarm setzen oder eine gute Alternative wählen.</p>
|
||||
)}
|
||||
<div class="status-cta">
|
||||
<a class="btn sm" href="/karte?produkt=midea-portasplit-12000">PortaSplit auf der Karte</a>
|
||||
<a class="btn sm ghost" href="/alarm?produkt=midea-portasplit-12000">Verfügbarkeits-Alarm</a>
|
||||
<a class="btn sm ghost" href="/portasplit">Alternativen ansehen</a>
|
||||
</div>
|
||||
</div></section>
|
||||
|
||||
<section class="wrap">
|
||||
<h2><span class="u"></span>Top-Empfehlungen</h2>
|
||||
<p class="muted">Kuratiert nach Bauart, Kühlleistung, Effizienz, Nutzungsfall und glaubwürdigen Quellen.</p>
|
||||
<div class="grid cards3">
|
||||
{top.map(p => <ProductCard product={p} />)}
|
||||
</div>
|
||||
<p style="margin-top:1rem"><a class="btn ghost" href="/geraete">Alle Geräte & Vergleich</a></p>
|
||||
</section>
|
||||
|
||||
<section class="wrap"><div class="split">
|
||||
<div class="card">
|
||||
<h2>PortaSplit ausverkauft?</h2>
|
||||
<p>Die Midea PortaSplit ist bei Hitze regelmäßig vergriffen. Wir zeigen dir wohnungstaugliche Alternativen, die wirklich kühlen – statt dich teuer hängen zu lassen.</p>
|
||||
<a class="btn ghost" href="/portasplit">Sinnvolle Alternativen</a>
|
||||
</div>
|
||||
<div class="card warm">
|
||||
<h2>Warum Kühlung wichtiger wird</h2>
|
||||
<p>Hitzewellen treten in Deutschland früher und intensiver auf. Gute, effiziente Kühlung kann Hitzeschutz sein – mit Quellen statt Panik.</p>
|
||||
<a class="btn ghost" href="/hitzeschutz">Hitzeschutz & Klima</a>
|
||||
</div>
|
||||
</div></section>
|
||||
</Base>
|
||||
<style>
|
||||
.hero{background:linear-gradient(180deg,var(--petrol-soft),var(--light));padding:3rem 0 2.5rem}
|
||||
.kicker{font-weight:600;color:var(--petrol-deep);margin:0 0 .4rem;text-transform:uppercase;letter-spacing:.04em;font-size:.8rem}
|
||||
.lead{font-size:1.15rem;max-width:46rem;color:var(--slate)}
|
||||
.modes{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem;margin-top:1.6rem}
|
||||
.mode{display:flex;flex-direction:column;gap:.2rem;background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:1.1rem;text-decoration:none;color:var(--ink);transition:border-color .2s,transform .08s}
|
||||
.mode:hover{border-color:var(--petrol);transform:translateY(-2px)}
|
||||
.mode strong{font-size:1.05rem;color:var(--petrol-deep)}
|
||||
.mode span{font-size:.9rem;color:var(--slate)}
|
||||
section.wrap{margin-top:2.5rem}
|
||||
.status{border-left:4px solid var(--warm)}
|
||||
.status-head{display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:.4rem}
|
||||
.status-grid{display:flex;gap:2rem;margin:.8rem 0;flex-wrap:wrap}
|
||||
.status-grid strong{font-size:1.3rem}
|
||||
.wucher{background:var(--danger-bg);color:var(--danger);padding:.8rem 1rem;border-radius:var(--radius);font-size:.95rem}
|
||||
.status-cta{display:flex;gap:.5rem;flex-wrap:wrap}
|
||||
.cards3{grid-template-columns:repeat(3,1fr)}
|
||||
.split{display:grid;grid-template-columns:1fr 1fr;gap:1rem}
|
||||
.card.warm{background:var(--warm-bg)}
|
||||
@media(max-width:820px){.modes,.cards3,.split{grid-template-columns:1fr}}
|
||||
</style>
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import { allProducts } from '../lib/pricing';
|
||||
const products = allProducts.map(p => ({ slug:p.slug, name:p.name }));
|
||||
---
|
||||
<Base title="Verfügbarkeits-Karte: portable Klimageräte in Baumärkten | Kühlfinder" description="Live-Karte: welche Baumärkte aktuell mobile Klimageräte auf Lager haben. Nach Gerät, Kette und Umkreis filtern.">
|
||||
<section class="wrap" style="margin-top:1.4rem">
|
||||
<p class="kicker"><span class="u"></span>Deutschlandkarte</p>
|
||||
<h1>Wo gibt es heute ein Klimagerät?</h1>
|
||||
<p class="muted">Märkte mit aktuellem Bestand. Wähle ein Gerät, nutze deinen Standort und stell den Umkreis ein.</p>
|
||||
</section>
|
||||
<section class="wrap mapwrap">
|
||||
<aside class="panel card">
|
||||
<label>Gerät
|
||||
<select id="f-product">
|
||||
<option value="all">Alle Geräte</option>
|
||||
{products.map(p => <option value={p.slug}>{p.name}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<button class="btn sm" id="locate">Meinen Standort nutzen</button>
|
||||
<label>Umkreis: <span id="r-out">150 km</span>
|
||||
<input type="range" id="f-radius" min="25" max="500" step="25" value="150" />
|
||||
</label>
|
||||
<div class="chains" id="f-chains">
|
||||
<strong class="note">Ketten</strong>
|
||||
<label><input type="checkbox" value="OBI" checked> OBI</label>
|
||||
<label><input type="checkbox" value="Hornbach" checked> Hornbach</label>
|
||||
<label><input type="checkbox" value="Bauhaus" checked> Bauhaus</label>
|
||||
<label><input type="checkbox" value="toom" checked> toom</label>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<span><i style="background:#1d9e75"></i>verfügbar</span>
|
||||
<span><i style="background:#ef9f27"></i>wenig</span>
|
||||
<span><i style="background:#b4b2a9"></i>leer</span>
|
||||
</div>
|
||||
<p class="note" id="count">…</p>
|
||||
<p class="note">Hinweis: aktuelle Demo-Daten. Live-Anbindung der Ketten-Bestände folgt.</p>
|
||||
</aside>
|
||||
<div id="map"></div>
|
||||
</section>
|
||||
</Base>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4.7.1/dist/maplibre-gl.css" />
|
||||
<style>
|
||||
.mapwrap{display:grid;grid-template-columns:280px 1fr;gap:1rem;margin-top:1rem;align-items:start}
|
||||
.panel{display:flex;flex-direction:column;gap:.9rem;position:sticky;top:74px}
|
||||
.panel label{display:block;font-weight:500;font-size:.9rem}
|
||||
.panel select,.panel input[type=range]{width:100%;margin-top:.3rem}
|
||||
.chains label{font-weight:400;display:flex;gap:.4rem;align-items:center;margin-top:.2rem}
|
||||
.legend{display:flex;gap:.8rem;flex-wrap:wrap;font-size:.82rem;color:var(--slate)}
|
||||
.legend i{display:inline-block;width:11px;height:11px;border-radius:50%;margin-right:.3rem;vertical-align:middle}
|
||||
#map{height:600px;border-radius:var(--radius);overflow:hidden;border:1px solid var(--line)}
|
||||
.maplibregl-popup-content{font-family:var(--font-body);border-radius:10px;padding:.7rem .8rem}
|
||||
.pop strong{font-size:.98rem}
|
||||
.pop .w{color:#a32d2d;font-weight:600}
|
||||
@media(max-width:820px){.mapwrap{grid-template-columns:1fr}.panel{position:static}#map{height:60vh}}
|
||||
</style>
|
||||
<script>
|
||||
import maplibregl from 'maplibre-gl';
|
||||
import storesData from '../data/stores.json';
|
||||
|
||||
type Offer = { productId:string; sourceType:string; sourceId:string; chain:string; price:number|null; availability:string; };
|
||||
const stores = storesData as any[];
|
||||
const AVAIL = ['in_stock','low_stock'];
|
||||
const params = new URLSearchParams(location.search);
|
||||
|
||||
const map = new maplibregl.Map({
|
||||
container:'map',
|
||||
style:'https://tiles.openfreemap.org/styles/liberty',
|
||||
center:[10.2,51.1], zoom:5.1
|
||||
});
|
||||
map.addControl(new maplibregl.NavigationControl(),'top-right');
|
||||
|
||||
let offers: Offer[] = [];
|
||||
let center: [number,number] | null = null;
|
||||
let markers: maplibregl.Marker[] = [];
|
||||
|
||||
const elProd = document.getElementById('f-product') as HTMLSelectElement;
|
||||
const elRad = document.getElementById('f-radius') as HTMLInputElement;
|
||||
const elROut = document.getElementById('r-out')!;
|
||||
const elCount = document.getElementById('count')!;
|
||||
const chainBoxes = Array.from(document.querySelectorAll('#f-chains input')) as HTMLInputElement[];
|
||||
if (params.get('produkt')) elProd.value = params.get('produkt')!;
|
||||
|
||||
function dist(aLat:number,aLng:number,bLat:number,bLng:number){
|
||||
const R=6371,dLat=(bLat-aLat)*Math.PI/180,dLng=(bLng-aLng)*Math.PI/180;
|
||||
const x=Math.sin(dLat/2)**2+Math.cos(aLat*Math.PI/180)*Math.cos(bLat*Math.PI/180)*Math.sin(dLng/2)**2;
|
||||
return 2*R*Math.asin(Math.sqrt(x));
|
||||
}
|
||||
const COL:any={in_stock:'#1d9e75',low_stock:'#ef9f27',out_of_stock:'#b4b2a9'};
|
||||
const fmt=(n:number|null)=> n==null?'–':new Intl.NumberFormat('de-DE',{style:'currency',currency:'EUR',maximumFractionDigits:0}).format(n);
|
||||
const slugByProductId:any = {};
|
||||
|
||||
function render(){
|
||||
markers.forEach(m=>m.remove()); markers=[];
|
||||
const prod = elProd.value;
|
||||
const radius = +elRad.value;
|
||||
elROut.textContent = radius+' km';
|
||||
const chains = chainBoxes.filter(c=>c.checked).map(c=>c.value);
|
||||
let shown=0, avail=0;
|
||||
for (const s of stores){
|
||||
if (!chains.includes(s.chain)) continue;
|
||||
if (center && dist(center[1],center[0],s.lat,s.lng)>radius) continue;
|
||||
const here = offers.filter(o=>o.sourceId===s.id && o.sourceType==='store' && (prod==='all'|| o.productId===prod));
|
||||
if (!here.length) continue;
|
||||
const best = here.find(o=>o.availability==='in_stock') || here.find(o=>o.availability==='low_stock') || here[0];
|
||||
const isAvail = AVAIL.includes(best.availability);
|
||||
shown++; if(isAvail) avail++;
|
||||
const el=document.createElement('div');
|
||||
el.style.cssText=`width:16px;height:16px;border-radius:50%;border:2px solid #fff;box-shadow:0 1px 3px rgba(0,0,0,.4);background:${COL[best.availability]||'#b4b2a9'}`;
|
||||
const list = here.filter(o=>AVAIL.includes(o.availability))
|
||||
.map(o=>`<div>${o.chain} · ${fmt(o.price)} · ${o.availability==='low_stock'?'wenig':'verfügbar'}</div>`).join('') || '<div class="muted">aktuell vergriffen</div>';
|
||||
const popup=new maplibregl.Popup({offset:14}).setHTML(`<div class="pop"><strong>${s.chain} ${s.city}</strong><br><span style="color:#666">${s.name}</span><br>${list}</div>`);
|
||||
const m=new maplibregl.Marker({element:el}).setLngLat([s.lng,s.lat]).setPopup(popup).addTo(map);
|
||||
markers.push(m);
|
||||
}
|
||||
elCount.textContent = `${avail} Märkte mit Bestand${center?` im Umkreis von ${radius} km`:''}`;
|
||||
}
|
||||
|
||||
document.getElementById('locate')!.addEventListener('click',()=>{
|
||||
if(!navigator.geolocation){elCount.textContent='Standort nicht verfügbar';return;}
|
||||
navigator.geolocation.getCurrentPosition(pos=>{
|
||||
center=[pos.coords.longitude,pos.coords.latitude];
|
||||
map.flyTo({center,zoom:7});
|
||||
new maplibregl.Marker({color:'#185fa5'}).setLngLat(center).addTo(map);
|
||||
render();
|
||||
},()=>{elCount.textContent='Standort abgelehnt – zeige ganz Deutschland';});
|
||||
});
|
||||
elProd.addEventListener('change',render);
|
||||
elRad.addEventListener('input',render);
|
||||
chainBoxes.forEach(c=>c.addEventListener('change',render));
|
||||
|
||||
fetch('/data/offers.json').then(r=>r.json()).then(d=>{offers=d.offers;render();}).catch(()=>{elCount.textContent='Daten konnten nicht geladen werden';});
|
||||
map.on('load',render);
|
||||
</script>
|
||||
@@ -0,0 +1,21 @@
|
||||
import { allProducts, priceIntel, euro } from '../lib/pricing';
|
||||
export const GET = () => {
|
||||
const lines = [
|
||||
'# Kühlfinder',
|
||||
'> Kuratierter Finder für portable Klimageräte in Deutschland mit Live-Verfügbarkeit in Baumärkten, Preis-Check (Wucher-Warnung) und Verfügbarkeits-Alarm.',
|
||||
'',
|
||||
'## Was Kühlfinder besser macht',
|
||||
'- Nur echte Kompressorgeräte (mobile Split, Monoblock) – keine Verdunstungs-/USB-Kühler.',
|
||||
'- Live-Verfügbarkeit pro Baumarkt-Filiale auf einer Deutschlandkarte (idealo zeigt keinen Filialbestand).',
|
||||
'- Warnung vor Wucherpreisen bei Knappheit (z. B. Midea PortaSplit).',
|
||||
'- Fokus auf wohnungstaugliche Geräte und PortaSplit-Alternativen.',
|
||||
'',
|
||||
'## Geräte',
|
||||
];
|
||||
for (const p of allProducts) {
|
||||
const pi = priceIntel(p);
|
||||
lines.push(`- ${p.name} (${p.type==='mobile_split'?'mobile Split':'Monoblock'}, ${p.btuh} BTU): ${p.shortVerdict} Üblicher Preis ${euro(pi.fairPrice)}. /geraete/${p.slug}`);
|
||||
}
|
||||
lines.push('', '## Seiten', '- /finder – 60-Sekunden-Empfehlung', '- /karte – Verfügbarkeits-Karte', '- /portasplit – PortaSplit-Status & Alternativen', '- /alarm – Verfügbarkeits-Alarm', '- /hitzeschutz – Klimawandel-Kontext', '- /transparenz – Methodik & Affiliate');
|
||||
return new Response(lines.join('\n'), { headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import ProductCard from '../components/ProductCard.astro';
|
||||
import { allProducts, getProduct, priceIntel, euro } from '../lib/pricing';
|
||||
const ps = getProduct('midea-portasplit-12000')!;
|
||||
const psi = priceIntel(ps);
|
||||
const alts = allProducts.filter(p=>['delonghi-pac-ex105','midea-portasplit-cool-8000','bosch-cool-5000','midea-silent-cool-26-pro-wf'].includes(p.id));
|
||||
const jsonld={'@context':'https://schema.org','@type':'FAQPage',mainEntity:[
|
||||
{'@type':'Question',name:'Was tun, wenn die Midea PortaSplit ausverkauft ist?',acceptedAnswer:{'@type':'Answer',text:'Sinnvolle Alternativen sind die kleinere PortaSplit Cool, der Monoblock De\'Longhi PAC EX105 oder die Bosch Cool 5000. Wer auf die PortaSplit warten will, nutzt den Verfügbarkeits-Alarm statt überteuert bei Drittanbietern zu kaufen.'}},
|
||||
{'@type':'Question',name:'Wie viel kostet die Midea PortaSplit normal?',acceptedAnswer:{'@type':'Answer',text:'Der übliche Preis liegt bei rund 899 € (UVP), im Angebot teils um 699 €. Preise deutlich darüber bei Drittanbietern sind Wucher.'}}
|
||||
]};
|
||||
---
|
||||
<Base title="Midea PortaSplit ausverkauft? Verfügbarkeit, Preis & beste Alternativen | Kühlfinder" description="PortaSplit-Status, Wucher-Warnung und wohnungstaugliche Alternativen, wenn die Midea PortaSplit nicht lieferbar ist." jsonld={jsonld}>
|
||||
<section class="wrap" style="margin-top:1.6rem">
|
||||
<p class="kicker"><span class="u"></span>PortaSplit-Hub</p>
|
||||
<h1>Midea PortaSplit: Status, Preis & sinnvolle Alternativen</h1>
|
||||
<div class="card" style="border-left:4px solid var(--warm)">
|
||||
<div class="sg">
|
||||
<div><span class="note">Üblicher Preis</span><br /><strong>{euro(psi.fairPrice)}</strong></div>
|
||||
<div><span class="note">Seriös ab</span><br /><strong>{euro(psi.lowestLegit)}</strong></div>
|
||||
<div><span class="note">Märkte mit Bestand</span><br /><strong>{psi.storesAvailable}</strong></div>
|
||||
</div>
|
||||
{psi.wucher && <p class="wucher">⚠ <strong>Wucher-Warnung:</strong> Drittanbieter verlangen bis {euro(psi.wucher.price)} – rund {Math.round((psi.wucher.price/psi.fairPrice-1)*100)} % über üblich. Lieber Alarm setzen oder Alternative wählen.</p>}
|
||||
<div class="status-cta">
|
||||
<a class="btn sm" href="/karte?produkt=midea-portasplit-12000">Auf der Karte</a>
|
||||
<a class="btn sm ghost" href="/alarm?produkt=midea-portasplit-12000">Alarm setzen</a>
|
||||
</div>
|
||||
</div>
|
||||
<h2 style="margin-top:1.6rem">Wohnungstaugliche Alternativen</h2>
|
||||
<p class="muted">Geräte, die in normalen Wohnungen wirklich kühlen – nicht nur „auch verfügbar“.</p>
|
||||
<div class="grid cards2">
|
||||
{alts.map(p=> <ProductCard product={p} />)}
|
||||
</div>
|
||||
</section>
|
||||
</Base>
|
||||
<style>
|
||||
.sg{display:flex;gap:2rem;flex-wrap:wrap;margin-bottom:.5rem}.sg strong{font-size:1.25rem}
|
||||
.wucher{background:var(--danger-bg);color:var(--danger);padding:.7rem .9rem;border-radius:var(--radius)}
|
||||
.status-cta{display:flex;gap:.5rem;flex-wrap:wrap}
|
||||
.cards2{grid-template-columns:1fr 1fr}
|
||||
@media(max-width:820px){.cards2{grid-template-columns:1fr}}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
export const GET = () => new Response(
|
||||
['User-agent: *','Allow: /','','User-agent: GPTBot','Allow: /','','User-agent: PerplexityBot','Allow: /','','Sitemap: https://kuehlfinder.de/sitemap-index.xml',''].join('\n'),
|
||||
{ headers: { 'Content-Type': 'text/plain; charset=utf-8' } }
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import { WUCHER_FACTOR } from '../lib/pricing';
|
||||
---
|
||||
<Base title="Transparenz & Methodik | Kühlfinder" description="Wie Kühlfinder bewertet, woher Preise und Verfügbarkeiten kommen und wie wir mit Affiliate umgehen.">
|
||||
<section class="wrap prose" style="margin-top:1.6rem;max-width:720px">
|
||||
<h1>Transparenz & Methodik</h1>
|
||||
<h2>Wie wir bewerten</h2>
|
||||
<p>Wir nehmen nur echte Kompressorgeräte auf (mobile Split, Monoblock). Die redaktionelle Einschätzung beruht auf Bauart, Kühlleistung, Effizienz, Lautstärke, Nutzungsfall, Verfügbarkeit und glaubwürdigen Testquellen (z. B. F.A.Z. Kaufkompass, Herstellerdaten, klimaaktiv). Das ist kein eigener Labortest.</p>
|
||||
<h2>Preise & Wucher-Warnung</h2>
|
||||
<p>Als „üblicher Preis“ dient die UVP bzw. der normale Marktpreis. Liegt ein verfügbares Angebot mehr als {Math.round((WUCHER_FACTOR-1)*100)} % darüber, markieren wir es als Wucher. Diese Angebote zählen nicht in den Durchschnitt.</p>
|
||||
<h2>Verfügbarkeit</h2>
|
||||
<p>Wir zeigen Verfügbarkeit pro Markt so aktuell wie möglich. Wenn Daten nicht automatisch geprüft sind, behaupten wir nie „verfügbar“, sondern nur „prüfen“. Preise und Bestände ändern sich während Hitzewellen sehr schnell.</p>
|
||||
<h2>Affiliate</h2>
|
||||
<p>Einige Links sind Affiliate-Links. Kaufst du darüber, erhalten wir ggf. eine Provision – für dich ändert sich der Preis nicht. Empfehlungen hängen nicht von der Provision ab.</p>
|
||||
</section>
|
||||
</Base>
|
||||
Reference in New Issue
Block a user