Kühlfinder MVP: Finder, Geräte, Karte (MapLibre), Preis-Intelligenz + Wucher-Warnung, PortaSplit-Hub, llms.txt
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
.astro
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.astro
|
||||||
|
.DS_Store
|
||||||
|
npm-debug.log*
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
# --- Build stage ---
|
||||||
|
FROM node:22-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# --- Serve stage ---
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
EXPOSE 80
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Kühlfinder
|
||||||
|
|
||||||
|
Portable Klimageräte finden + Live-Verfügbarkeit (Deutschlandkarte), Preis-Check mit Wucher-Warnung, Verfügbarkeits-Alarm. Astro 5, statisch, Docker/nginx.
|
||||||
|
|
||||||
|
## Dev
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Daten: src/data/*.json (Build-Zeit) + public/data/offers.json (Client/Karte). Preislogik: src/lib/pricing.ts (Wucher = Preis > UVP*1.3).
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
export default defineConfig({
|
||||||
|
site: 'https://kuehlfinder.de',
|
||||||
|
output: 'static',
|
||||||
|
build: { inlineStylesheets: 'auto' }
|
||||||
|
});
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
location / { try_files $uri $uri/ $uri.html /404.html; }
|
||||||
|
location = /llms.txt { default_type text/plain; }
|
||||||
|
location = /robots.txt { default_type text/plain; }
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
add_header Referrer-Policy strict-origin-when-cross-origin;
|
||||||
|
add_header X-Frame-Options SAMEORIGIN;
|
||||||
|
}
|
||||||
Generated
+5864
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "kuehlfinder",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "^5.7.0",
|
||||||
|
"@fontsource/fraunces": "^5.0.0",
|
||||||
|
"@fontsource/public-sans": "^5.0.0",
|
||||||
|
"maplibre-gl": "^4.7.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
import { priceIntel, euro, type Product } from '../lib/pricing';
|
||||||
|
const { product, compact } = Astro.props as { product: Product; compact?: boolean };
|
||||||
|
const pi = priceIntel(product);
|
||||||
|
const typeLabel = product.type === 'mobile_split' ? 'Mobile Split' : 'Monoblock';
|
||||||
|
---
|
||||||
|
<article class="card pc">
|
||||||
|
<div class="pc-top">
|
||||||
|
<span class="badge">{product.badge}</span>
|
||||||
|
{product.apartmentSuitable
|
||||||
|
? <span class="badge tag-ok">wohnungstauglich</span>
|
||||||
|
: <span class="badge">Spezialfall</span>}
|
||||||
|
</div>
|
||||||
|
<h3><a href={`/geraete/${product.slug}`}>{product.name}</a></h3>
|
||||||
|
<p class="muted">{typeLabel} · {product.btuh.toLocaleString('de-DE')} BTU · {product.roomSizeM2[0]}–{product.roomSizeM2[1]} m²</p>
|
||||||
|
{!compact && <p>{product.shortVerdict}</p>}
|
||||||
|
<div class="pc-price">
|
||||||
|
<div><span class="note">Üblicher Preis</span><br /><strong>{euro(pi.fairPrice)}</strong></div>
|
||||||
|
<div><span class="note">Aktuell ab</span><br /><strong>{euro(pi.lowestLegit)}</strong></div>
|
||||||
|
</div>
|
||||||
|
<div class="pc-avail">
|
||||||
|
{pi.storesAvailable > 0
|
||||||
|
? <span class="badge tag-ok">In {pi.storesAvailable} Märkten verfügbar</span>
|
||||||
|
: (pi.onlineAvailable
|
||||||
|
? <span class="badge tag-warn">Aktuell nur online</span>
|
||||||
|
: (pi.preorder
|
||||||
|
? <span class="badge tag-warn">Vorbestellbar</span>
|
||||||
|
: <span class="badge tag-danger">Aktuell vergriffen</span>))}
|
||||||
|
{pi.wucher && <span class="badge tag-danger">⚠ Wucher: bis {euro(pi.wucher.price)}</span>}
|
||||||
|
</div>
|
||||||
|
<div class="pc-cta">
|
||||||
|
<a class="btn sm" href={`/karte?produkt=${product.slug}`}>Verfügbarkeit auf Karte</a>
|
||||||
|
<a class="btn sm ghost" href={`/geraete/${product.slug}`}>Details</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<style>
|
||||||
|
.pc{display:flex;flex-direction:column;gap:.5rem}
|
||||||
|
.pc-top{display:flex;gap:.4rem;flex-wrap:wrap}
|
||||||
|
.pc h3{margin:.2rem 0 0}
|
||||||
|
.pc h3 a{color:var(--ink);text-decoration:none}
|
||||||
|
.pc h3 a:hover{color:var(--petrol)}
|
||||||
|
.pc-price{display:flex;gap:1.5rem;margin:.3rem 0;border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:.6rem 0}
|
||||||
|
.pc-price strong{font-size:1.15rem}
|
||||||
|
.pc-avail{display:flex;gap:.4rem;flex-wrap:wrap}
|
||||||
|
.pc-cta{display:flex;gap:.5rem;margin-top:.4rem;flex-wrap:wrap}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
|||||||
|
[
|
||||||
|
{"id":"idealo","name":"idealo","kind":"preisvergleich"},
|
||||||
|
{"id":"amazon","name":"Amazon","kind":"online"},
|
||||||
|
{"id":"mediamarkt","name":"MediaMarkt","kind":"online"}
|
||||||
|
]
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id":"midea-portasplit-12000","slug":"midea-portasplit-12000","name":"Midea PortaSplit 12.000 BTU","brand":"Midea",
|
||||||
|
"type":"mobile_split","tier":"A","badge":"Beste mobile Split-Wahl","hero":true,"apartmentSuitable":true,
|
||||||
|
"shortVerdict":"Die beliebteste mobile Splitanlage – effizient und leise, wenn du das Außengerät sicher platzieren kannst.",
|
||||||
|
"coolingKw":3.5,"btuh":12000,"efficiencyCooling":"A++","noiseNote":"Silent-Modus ca. 39 dB(A) laut Händler",
|
||||||
|
"roomSizeM2":[18,42],"uvp":899,"fairPriceMax":899,
|
||||||
|
"bestFor":["Schlafzimmer","Homeoffice","Mietwohnung mit Balkon/Fensterbank","Räume bis 42 m²"],
|
||||||
|
"notFor":["Keine sichere Stellfläche fürs Außengerät","sehr kleines Budget"],
|
||||||
|
"pros":["Mobile Split-Bauweise ohne feste Installation","Sehr gute Kühlleistung","Effizienter als Monoblocks","Heizfunktion"],
|
||||||
|
"cons":["Teuer und oft knapp","Groß und schwer","Außengerät muss sicher stehen"],
|
||||||
|
"score":94,"sources":[{"label":"Midea Produktseite","url":"https://www.midea.com/de/klimatisieren-heizen/portasplit"}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":"midea-portasplit-cool-8000","slug":"midea-portasplit-cool-8000","name":"Midea PortaSplit Cool 8.000 BTU","brand":"Midea",
|
||||||
|
"type":"mobile_split","tier":"A","badge":"Beste Wahl für kleine Schlafräume","apartmentSuitable":true,
|
||||||
|
"shortVerdict":"Die kleinere, günstigere PortaSplit – ideal für kleine bis mittlere Schlafzimmer.",
|
||||||
|
"coolingKw":2.3,"btuh":8000,"efficiencyCooling":"A++","noiseNote":"Silent-Modus ca. 38 dB(A) laut Händler",
|
||||||
|
"roomSizeM2":[12,28],"uvp":749,"fairPriceMax":749,
|
||||||
|
"bestFor":["Kleine Schlafzimmer","Räume bis 28 m²","Mietwohnung mit Balkon/Fensterbank"],
|
||||||
|
"notFor":["Große oder stark aufgeheizte Räume","keine Stellfläche fürs Außengerät"],
|
||||||
|
"pros":["Mobile Split-Bauweise","Leichter und günstiger als die große PortaSplit","Leise im Silent-Modus"],
|
||||||
|
"cons":["Weniger Leistung","ebenfalls oft knapp","Außengerät nötig"],
|
||||||
|
"score":90,"sources":[{"label":"Midea PortaSplit Cool","url":"https://www.midea.com/de/klimatisieren-heizen/portasplit-cool.portasplit"}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":"delonghi-pac-ex105","slug":"delonghi-pac-ex105","name":"De'Longhi Pinguino PAC EX105","brand":"De'Longhi",
|
||||||
|
"type":"monoblock","tier":"A","badge":"Beste PortaSplit-Alternative ohne Außengerät","apartmentSuitable":true,
|
||||||
|
"shortVerdict":"Testsieger-Monoblock: starke Kühlung bei niedrigem Verbrauch – die beste Wahl, wenn kein Außengerät möglich ist.",
|
||||||
|
"coolingKw":2.5,"btuh":10000,"efficiencyCooling":"A+++","noiseNote":"ca. 63 dB(A) Schallleistung (klimaaktiv)",
|
||||||
|
"roomSizeM2":[20,40],"uvp":699,"fairPriceMax":749,
|
||||||
|
"bestFor":["Mietwohnung ohne Stellplatz fürs Außengerät","Schlafzimmer/Homeoffice mit Abdichtung","Vielnutzer"],
|
||||||
|
"notFor":["Wer maximale Leise-Werte braucht"],
|
||||||
|
"pros":["F.A.Z.-Testsieger Monoblock","Sehr effizient (EER 4.1, R290)","Gute Verarbeitung"],
|
||||||
|
"cons":["Teuer","Groß und schwer","Fensterabdichtung meist separat nötig"],
|
||||||
|
"score":89,"sources":[{"label":"klimaaktiv Topprodukte","url":"https://www.klimaaktiv.at/private/topprodukte/detail/delonghi-pac-ex105-477"}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":"bosch-cool-5000","slug":"bosch-cool-5000","name":"Bosch Cool 5000","brand":"Bosch",
|
||||||
|
"type":"monoblock","tier":"A","badge":"Premium-Komfort mit Inverter","apartmentSuitable":true,
|
||||||
|
"shortVerdict":"Modernes Monoblock-Gerät mit seltener Inverter-Technik – stark für größere Räume.",
|
||||||
|
"coolingKw":3.4,"btuh":11600,"efficiencyCooling":"A+","noiseNote":"ca. 64 dB(A) Schallleistung – für Schlafräume kritisch prüfen",
|
||||||
|
"roomSizeM2":[25,46],"uvp":799,"fairPriceMax":849,
|
||||||
|
"bestFor":["Homeoffice","Wohnzimmer","größere Räume","Komfort/Energieeffizienz"],
|
||||||
|
"notFor":["Sehr leise Schlafräume"],
|
||||||
|
"pros":["Inverter-Technik (bei Monoblocks selten)","Hohe Kühlleistung","Markengerät"],
|
||||||
|
"cons":["Monoblock mit Nachström-Problem","relativ laut","teuer"],
|
||||||
|
"score":85,"sources":[{"label":"Bosch Home Comfort","url":"https://www.bosch-homecomfort.com/de/de/wohngebaeude/unternehmen/presse/neuheit-cool-5000/"}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":"aeg-comfort-6000","slug":"aeg-comfort-6000","name":"AEG Comfort 6000","brand":"AEG",
|
||||||
|
"type":"monoblock","tier":"B","badge":"Smart & App","apartmentSuitable":true,
|
||||||
|
"shortVerdict":"Gute App-Steuerung und Verarbeitung – die Smart-Wahl, aber nicht die effizienteste.",
|
||||||
|
"coolingKw":2.6,"btuh":9000,"efficiencyCooling":"A","noiseNote":"mittel",
|
||||||
|
"roomSizeM2":[18,34],"uvp":649,"fairPriceMax":699,
|
||||||
|
"bestFor":["App-Steuerung","Design/Komfort","Homeoffice"],
|
||||||
|
"notFor":["Wer auf niedrigen Stromverbrauch achtet"],
|
||||||
|
"pros":["Sehr gute App","Gute Verarbeitung","Gute Kühlleistung"],
|
||||||
|
"cons":["Hoher Stromverbrauch (F.A.Z.)","kein Fenster-Kit im Lieferumfang","teuer"],
|
||||||
|
"score":80,"sources":[{"label":"F.A.Z. Kaufkompass","url":"https://www.faz.net/kaufkompass/test/die-beste-mobile-klimaanlage/"}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":"dreo-318s","slug":"dreo-318s","name":"Dreo 318S","brand":"Dreo",
|
||||||
|
"type":"monoblock","tier":"B","badge":"Kompakt für kleine Räume","apartmentSuitable":true,
|
||||||
|
"shortVerdict":"Kompakte Kühlung mit guter App – sinnvoll für kleinere Räume.",
|
||||||
|
"coolingKw":2.1,"btuh":8000,"efficiencyCooling":"A","noiseNote":"mittel",
|
||||||
|
"roomSizeM2":[10,22],"uvp":429,"fairPriceMax":469,
|
||||||
|
"bestFor":["Kleine Räume","wenig Stellplatz"],
|
||||||
|
"notFor":["Große, stark aufgeheizte Räume"],
|
||||||
|
"pros":["Klein und leicht","Gute App-Anbindung","Günstiger als Premium-Monoblocks"],
|
||||||
|
"cons":["Begrenzte Leistung","Verfügbarkeit prüfen"],
|
||||||
|
"score":76,"sources":[{"label":"F.A.Z. Kaufkompass","url":"https://www.faz.net/kaufkompass/test/die-beste-mobile-klimaanlage/"}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":"midea-silent-cool-26-pro-wf","slug":"midea-silent-cool-26-pro-wf","name":"Midea Silent Cool 26 Pro WF","brand":"Midea",
|
||||||
|
"type":"monoblock","tier":"B","badge":"Preis-Leistung","apartmentSuitable":true,
|
||||||
|
"shortVerdict":"Günstigere Midea-Alternative mit App – solide Zwischenlösung, wenn die PortaSplit weg ist.",
|
||||||
|
"coolingKw":2.6,"btuh":9000,"efficiencyCooling":"A","noiseNote":"im Schlafzimmer kritisch prüfen",
|
||||||
|
"roomSizeM2":[16,30],"uvp":499,"fairPriceMax":549,
|
||||||
|
"bestFor":["Budget unter 600 €","Markengerät","PortaSplit-Ausfall-Alternative"],
|
||||||
|
"notFor":["Höchste Leise-Ansprüche"],
|
||||||
|
"pros":["Günstigere Midea-Option","App/WiFi","Gute Zwischenlösung"],
|
||||||
|
"cons":["Kein Split-Niveau","Fensterabdichtung Pflicht"],
|
||||||
|
"score":78,"sources":[{"label":"F.A.Z. Kaufkompass","url":"https://www.faz.net/kaufkompass/test/die-beste-mobile-klimaanlage/"}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":"comfee-mobile-12000","slug":"comfee-mobile-12000","name":"Comfee Mobile 12000","brand":"Comfee",
|
||||||
|
"type":"monoblock","tier":"C","badge":"Budget-Notfalloption","apartmentSuitable":true,
|
||||||
|
"shortVerdict":"Brauchbare Budget-Option, wenn Premiumgeräte weg sind – aber kein Premium.",
|
||||||
|
"coolingKw":3.5,"btuh":12000,"efficiencyCooling":"A","noiseNote":"eher laut",
|
||||||
|
"roomSizeM2":[20,35],"uvp":379,"fairPriceMax":429,
|
||||||
|
"bestFor":["Akutkauf mit kleinem Budget","schnelle Notlösung"],
|
||||||
|
"notFor":["Schlafräume mit Leise-Anspruch","Premium-Erwartung"],
|
||||||
|
"pros":["Günstig","12.000-BTU-Klasse","oft verfügbar"],
|
||||||
|
"cons":["Lauter, weniger effizient","Monoblock"],
|
||||||
|
"score":68,"sources":[{"label":"F.A.Z. Kaufkompass","url":"https://www.faz.net/kaufkompass/test/die-beste-mobile-klimaanlage/"}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":"remko-rkl-495-dc","slug":"remko-rkl-495-dc","name":"Remko RKL 495 DC","brand":"Remko",
|
||||||
|
"type":"mobile_split","tier":"B","badge":"Premium-Spezialfall","apartmentSuitable":false,
|
||||||
|
"shortVerdict":"Sehr leistungsstarke mobile Split – Premium-Nische für große Räume und hohes Budget.",
|
||||||
|
"coolingKw":4.4,"btuh":15000,"efficiencyCooling":"A+","noiseNote":"mittel",
|
||||||
|
"roomSizeM2":[35,60],"uvp":1599,"fairPriceMax":1699,
|
||||||
|
"bestFor":["Große Räume","hohes Budget","mobile Split-Bauweise"],
|
||||||
|
"notFor":["Schnellkauf","kleines Budget","kleine Räume"],
|
||||||
|
"pros":["Mobile Split","sehr leistungsstark"],
|
||||||
|
"cons":["Sehr teuer","groß/schwer"],
|
||||||
|
"score":79,"sources":[{"label":"F.A.Z. Kaufkompass","url":"https://www.faz.net/kaufkompass/test/die-beste-mobile-klimaanlage/"}]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
[
|
||||||
|
{"id":"obi-hamburg","chain":"OBI","name":"OBI Hamburg-Wandsbek","plz":"22041","city":"Hamburg","lat":53.573,"lng":10.085},
|
||||||
|
{"id":"hornbach-hamburg","chain":"Hornbach","name":"Hornbach Hamburg-Wandsbek","plz":"22047","city":"Hamburg","lat":53.585,"lng":10.103},
|
||||||
|
{"id":"bauhaus-berlin","chain":"Bauhaus","name":"Bauhaus Berlin-Halensee","plz":"10711","city":"Berlin","lat":52.497,"lng":13.290},
|
||||||
|
{"id":"obi-berlin","chain":"OBI","name":"OBI Berlin-Lichtenberg","plz":"10365","city":"Berlin","lat":52.512,"lng":13.500},
|
||||||
|
{"id":"toom-koeln","chain":"toom","name":"toom Köln-Ehrenfeld","plz":"50825","city":"Köln","lat":50.952,"lng":6.917},
|
||||||
|
{"id":"hornbach-koeln","chain":"Hornbach","name":"Hornbach Köln-Godorf","plz":"50997","city":"Köln","lat":50.864,"lng":6.987},
|
||||||
|
{"id":"bauhaus-muenchen","chain":"Bauhaus","name":"Bauhaus München-Laim","plz":"80687","city":"München","lat":48.140,"lng":11.500},
|
||||||
|
{"id":"obi-muenchen","chain":"OBI","name":"OBI München-Pasing","plz":"81241","city":"München","lat":48.146,"lng":11.460},
|
||||||
|
{"id":"hornbach-frankfurt","chain":"Hornbach","name":"Hornbach Frankfurt-Bornheim","plz":"60385","city":"Frankfurt","lat":50.131,"lng":8.713},
|
||||||
|
{"id":"toom-stuttgart","chain":"toom","name":"toom Stuttgart-Vaihingen","plz":"70563","city":"Stuttgart","lat":48.728,"lng":9.110},
|
||||||
|
{"id":"bauhaus-stuttgart","chain":"Bauhaus","name":"Bauhaus Stuttgart-Feuerbach","plz":"70469","city":"Stuttgart","lat":48.808,"lng":9.157},
|
||||||
|
{"id":"obi-hannover","chain":"OBI","name":"OBI Hannover-Vahrenheide","plz":"30179","city":"Hannover","lat":52.420,"lng":9.760},
|
||||||
|
{"id":"hornbach-dresden","chain":"Hornbach","name":"Hornbach Dresden-Kaditz","plz":"01139","city":"Dresden","lat":51.082,"lng":13.690},
|
||||||
|
{"id":"bauhaus-leipzig","chain":"Bauhaus","name":"Bauhaus Leipzig-Paunsdorf","plz":"04329","city":"Leipzig","lat":51.350,"lng":12.470},
|
||||||
|
{"id":"obi-dortmund","chain":"OBI","name":"OBI Dortmund-Aplerbeck","plz":"44287","city":"Dortmund","lat":51.490,"lng":7.560},
|
||||||
|
{"id":"toom-nuernberg","chain":"toom","name":"toom Nürnberg-Langwasser","plz":"90471","city":"Nürnberg","lat":49.400,"lng":11.130},
|
||||||
|
{"id":"bauhaus-karlsruhe","chain":"Bauhaus","name":"Bauhaus Karlsruhe-Durlach","plz":"76227","city":"Karlsruhe","lat":48.998,"lng":8.470},
|
||||||
|
{"id":"hornbach-bremen","chain":"Hornbach","name":"Hornbach Bremen-Habenhausen","plz":"28279","city":"Bremen","lat":53.040,"lng":8.840}
|
||||||
|
]
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
const { title, description, jsonld } = Astro.props;
|
||||||
|
const desc = description ?? 'Kühlfinder – das richtige portable Klimagerät finden und sehen, wo es heute verfügbar ist.';
|
||||||
|
import '../styles/global.css';
|
||||||
|
---
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>{title}</title>
|
||||||
|
<meta name="description" content={desc} />
|
||||||
|
<link rel="canonical" href={Astro.url.href} />
|
||||||
|
<meta property="og:title" content={title} />
|
||||||
|
<meta property="og:description" content={desc} />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
{jsonld && <script type="application/ld+json" set:html={JSON.stringify(jsonld)} />}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="site"><div class="wrap bar">
|
||||||
|
<a class="brand" href="/">Kühl<b>finder</b></a>
|
||||||
|
<nav>
|
||||||
|
<a href="/finder">Finder</a>
|
||||||
|
<a href="/geraete">Geräte</a>
|
||||||
|
<a href="/karte">Karte</a>
|
||||||
|
<a href="/portasplit">PortaSplit</a>
|
||||||
|
</nav>
|
||||||
|
</div></header>
|
||||||
|
<main><slot /></main>
|
||||||
|
<footer class="site"><div class="wrap">
|
||||||
|
<p class="note"><strong>Transparenz:</strong> Einige Links sind Affiliate-Links – kaufst du darüber, erhalten wir ggf. eine Provision, für dich ändert sich der Preis nicht. Empfehlungen basieren auf Nutzungsfall, Technik, Testquellen und redaktioneller Einschätzung – nicht auf Provision.</p>
|
||||||
|
<p class="note">Preise und Verfügbarkeit ändern sich während Hitzewellen sehr schnell. Wir halten Daten so aktuell wie möglich, geben aber keine Garantie. Bitte beim Händler final prüfen.</p>
|
||||||
|
<p class="note"><a href="/transparenz">Transparenz & Methodik</a> · <a href="/impressum">Impressum</a> · <a href="/datenschutz">Datenschutz</a> · © {new Date().getFullYear()} Kühlfinder</p>
|
||||||
|
</div></footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import products from '../data/products.json';
|
||||||
|
import stores from '../data/stores.json';
|
||||||
|
import offersData from '../data/offers.json';
|
||||||
|
|
||||||
|
export type Product = (typeof products)[number];
|
||||||
|
export type Store = (typeof stores)[number];
|
||||||
|
export interface Offer {
|
||||||
|
productId: string; sourceType: 'store' | 'online'; sourceId: string; chain: string;
|
||||||
|
price: number | null; availability: string; availabilityNote?: string; url: string; checkedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const allProducts = products as Product[];
|
||||||
|
export const allStores = stores as Store[];
|
||||||
|
export const allOffers = (offersData as any).offers as Offer[];
|
||||||
|
export const dataUpdatedAt = (offersData as any).generatedAt as string;
|
||||||
|
|
||||||
|
const AVAILABLE = new Set(['in_stock', 'low_stock', 'online_available', 'store_pickup_available']);
|
||||||
|
export const WUCHER_FACTOR = 1.3;
|
||||||
|
|
||||||
|
export function getProduct(slug: string) {
|
||||||
|
return allProducts.find((p) => p.slug === slug);
|
||||||
|
}
|
||||||
|
export function offersFor(productId: string) {
|
||||||
|
return allOffers.filter((o) => o.productId === productId);
|
||||||
|
}
|
||||||
|
export function storeById(id: string) {
|
||||||
|
return allStores.find((s) => s.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PriceIntel {
|
||||||
|
fairPrice: number; // redaktionelle Referenz (UVP/üblicher Preis)
|
||||||
|
lowestLegit: number | null; // günstigstes seriöses Angebot
|
||||||
|
avgLegit: number | null; // Durchschnitt seriöser Angebote
|
||||||
|
storesAvailable: number; // Filialen mit Bestand
|
||||||
|
onlineAvailable: boolean;
|
||||||
|
preorder: boolean;
|
||||||
|
wucher: { price: number; chain: string } | null; // höchstes Wucher-Angebot
|
||||||
|
}
|
||||||
|
|
||||||
|
export function priceIntel(p: Product): PriceIntel {
|
||||||
|
const offers = offersFor(p.id);
|
||||||
|
const fairPrice = p.uvp;
|
||||||
|
const wucherThreshold = p.uvp * WUCHER_FACTOR;
|
||||||
|
const legitPrices: number[] = [];
|
||||||
|
let storesAvailable = 0, onlineAvailable = false, preorder = false;
|
||||||
|
let wucher: { price: number; chain: string } | null = null;
|
||||||
|
for (const o of offers) {
|
||||||
|
const isAvail = AVAILABLE.has(o.availability);
|
||||||
|
if (o.availability === 'preorder') preorder = true;
|
||||||
|
if (o.price != null && o.price > wucherThreshold && isAvail) {
|
||||||
|
if (!wucher || o.price > wucher.price) wucher = { price: o.price, chain: o.chain };
|
||||||
|
continue; // Wucher zählt nicht zum Durchschnitt
|
||||||
|
}
|
||||||
|
if (o.sourceType === 'store' && isAvail) storesAvailable++;
|
||||||
|
if (o.sourceType === 'online' && isAvail) onlineAvailable = true;
|
||||||
|
if (o.price != null && o.price <= wucherThreshold) legitPrices.push(o.price);
|
||||||
|
}
|
||||||
|
const lowestLegit = legitPrices.length ? Math.min(...legitPrices) : null;
|
||||||
|
const avgLegit = legitPrices.length ? Math.round(legitPrices.reduce((a, b) => a + b, 0) / legitPrices.length) : null;
|
||||||
|
return { fairPrice, lowestLegit, avgLegit, storesAvailable, onlineAvailable, preorder, wucher };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function euro(n: number | null) {
|
||||||
|
return n == null ? '–' : new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(n);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
@import '@fontsource/fraunces/400.css';
|
||||||
|
@import '@fontsource/fraunces/600.css';
|
||||||
|
@import '@fontsource/public-sans/400.css';
|
||||||
|
@import '@fontsource/public-sans/500.css';
|
||||||
|
@import '@fontsource/public-sans/600.css';
|
||||||
|
|
||||||
|
:root{
|
||||||
|
--petrol: oklch(0.46 0.066 207);
|
||||||
|
--petrol-deep: oklch(0.36 0.06 210);
|
||||||
|
--petrol-soft: oklch(0.95 0.018 200);
|
||||||
|
--ink: oklch(0.24 0.02 250);
|
||||||
|
--slate: oklch(0.52 0.02 250);
|
||||||
|
--light: oklch(0.985 0.006 230);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--line: oklch(0.9 0.01 240);
|
||||||
|
--warm: oklch(0.68 0.15 47);
|
||||||
|
--warm-bg: oklch(0.96 0.035 60);
|
||||||
|
--danger: oklch(0.55 0.18 25);
|
||||||
|
--danger-bg: oklch(0.95 0.04 30);
|
||||||
|
--ok: oklch(0.55 0.11 155);
|
||||||
|
--ok-bg: oklch(0.95 0.04 160);
|
||||||
|
--radius: 12px;
|
||||||
|
--maxw: 1100px;
|
||||||
|
--font-head: 'Fraunces', Georgia, serif;
|
||||||
|
--font-body: 'Public Sans', system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
html{scroll-behavior:smooth}
|
||||||
|
body{margin:0;font-family:var(--font-body);color:var(--ink);background:var(--light);line-height:1.6;-webkit-font-smoothing:antialiased}
|
||||||
|
h1,h2,h3{font-family:var(--font-head);font-weight:600;line-height:1.15;margin:0 0 .5em}
|
||||||
|
h1{font-size:clamp(2rem,5vw,3.1rem);letter-spacing:-.01em}
|
||||||
|
h2{font-size:clamp(1.5rem,3vw,2.1rem)}
|
||||||
|
h3{font-size:1.2rem}
|
||||||
|
p{margin:0 0 1rem}
|
||||||
|
a{color:var(--petrol);text-underline-offset:3px}
|
||||||
|
.wrap{max-width:var(--maxw);margin:0 auto;padding:0 1.2rem}
|
||||||
|
.u{display:inline-block;height:.18em;width:2.2rem;background:var(--warm);vertical-align:middle;margin-right:.5rem;border-radius:2px}
|
||||||
|
.btn{display:inline-flex;align-items:center;gap:.5rem;font-weight:600;font-family:var(--font-body);padding:.8rem 1.3rem;border-radius:999px;border:1.5px solid var(--petrol);background:var(--petrol);color:#fff;text-decoration:none;cursor:pointer;transition:transform .08s ease,background .2s}
|
||||||
|
.btn:hover{background:var(--petrol-deep);border-color:var(--petrol-deep)}
|
||||||
|
.btn:active{transform:scale(.98)}
|
||||||
|
.btn.ghost{background:transparent;color:var(--petrol)}
|
||||||
|
.btn.ghost:hover{background:var(--petrol-soft)}
|
||||||
|
.btn.sm{padding:.5rem .9rem;font-size:.9rem}
|
||||||
|
.card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:1.2rem}
|
||||||
|
.badge{display:inline-block;font-size:.78rem;font-weight:600;padding:.2rem .6rem;border-radius:999px;background:var(--petrol-soft);color:var(--petrol-deep)}
|
||||||
|
.tag-ok{background:var(--ok-bg);color:var(--ok)}
|
||||||
|
.tag-warn{background:var(--warm-bg);color:oklch(0.45 0.12 47)}
|
||||||
|
.tag-danger{background:var(--danger-bg);color:var(--danger)}
|
||||||
|
.muted{color:var(--slate)}
|
||||||
|
.grid{display:grid;gap:1rem}
|
||||||
|
.note{font-size:.85rem;color:var(--slate)}
|
||||||
|
header.site{position:sticky;top:0;z-index:50;background:oklch(0.985 0.006 230 / .92);backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}
|
||||||
|
header.site .bar{display:flex;align-items:center;justify-content:space-between;height:62px}
|
||||||
|
header.site nav a{margin-left:1.2rem;text-decoration:none;color:var(--ink);font-weight:500;font-size:.95rem}
|
||||||
|
header.site nav a:hover{color:var(--petrol)}
|
||||||
|
.brand{font-family:var(--font-head);font-weight:600;font-size:1.25rem;color:var(--ink);text-decoration:none}
|
||||||
|
.brand b{color:var(--petrol)}
|
||||||
|
footer.site{margin-top:4rem;border-top:1px solid var(--line);background:var(--petrol-soft)}
|
||||||
|
footer.site .wrap{padding-top:2rem;padding-bottom:2rem}
|
||||||
|
@media(max-width:720px){header.site nav a{margin-left:.8rem;font-size:.85rem}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{ "extends": "astro/tsconfigs/strict" }
|
||||||
Reference in New Issue
Block a user