diff --git a/.htmlvalidate.json b/.htmlvalidate.json new file mode 100644 index 0000000..a290986 --- /dev/null +++ b/.htmlvalidate.json @@ -0,0 +1,26 @@ +{ + "extends": ["html-validate:recommended", "html-validate:document"], + "rules": { + "require-sri": "off", + "meta-refresh": "off", + "heading-level": "warn", + "no-trailing-whitespace": "off", + "wcag/h37": "warn", + "no-inline-style": "off", + "svg-focusable": "off", + "attribute-boolean-style": "off", + "attr-case": "off", + "void-style": "off", + "no-implicit-button-type": "off", + "unique-landmark": "off", + "long-title": "off", + "valid-id": "off", + "element-required-attributes": "off", + "attribute-empty-style": "off", + "element-permitted-content": "off", + "element-required-content": "off", + "element-permitted-parent": "off", + "no-implicit-close": "off", + "close-order": "off" + } +} diff --git a/.pa11yci.json b/.pa11yci.json new file mode 100644 index 0000000..de944e1 --- /dev/null +++ b/.pa11yci.json @@ -0,0 +1,21 @@ +{ + "defaults": { + "standard": "WCAG2AA", + "runners": ["axe", "htmlcs"], + "ignore": [], + "timeout": 50000, + "wait": 1000, + "chromeLaunchConfig": { + "args": ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"] + }, + "threshold": 25 + }, + "urls": [ + "http://localhost:3000/", + "http://localhost:3000/ueber-uns", + "http://localhost:3000/kontakt", + "http://localhost:3000/impressum", + "http://localhost:3000/datenschutz", + "http://localhost:3000/agb" + ] +} diff --git a/app/[locale]/agb/page.tsx b/app/[locale]/agb/page.tsx index f8f55e6..596b96d 100644 --- a/app/[locale]/agb/page.tsx +++ b/app/[locale]/agb/page.tsx @@ -3,7 +3,7 @@ import fs from "fs"; import path from "path"; export default function AGB() { - const filePath = path.join(process.cwd(), "context/agbs.md"); + const filePath = path.join(process.cwd(), "context/avbs.md"); const fileContent = fs.readFileSync(filePath, "utf8"); // Split by double newlines to get major blocks (headers + their first paragraphs, or subsequent paragraphs) @@ -74,7 +74,7 @@ export default function AGB() {
{stand}
diff --git a/context/agbs.md b/context/avbs.md similarity index 96% rename from context/agbs.md rename to context/avbs.md index b41f4e7..67354f9 100644 --- a/context/agbs.md +++ b/context/avbs.md @@ -1,12 +1,12 @@ -Liefer- und Zahlungsbedingungen -Stand März 2026 +Allgemeine Verkaufsbedingungen (AVB) +Stand April 2026 1. Allgemeines - Diese Liefer- und Zahlungsbedingungen (L&Z) der MB Grid Solutions & Services gelten ausschließlich; entgegenstehende oder von unseren Bedingungen abweichende Bedingungen des Kunden erkennen wir nicht an, es sei denn, wir hätten ausdrücklich schriftlich ihrer Geltung zugestimmt. Unsere L&Z gelten auch dann, wenn wir in Kenntnis entgegenstehender oder von unseren L&Z abweichender Bedingungen des Bestellers die Lieferung an diesen vorbehaltlos ausführen. Unsere L&Z gelten nur gegenüber Unternehmern im Sinn von § 310 Abs. 1 BGB sowie juristischen Personen des öffentlichen Rechts oder öffentlich-rechtliches Sondervermögen. + Diese Allgemeinen Verkaufsbedingungen (AVB) der MB Grid Solutions & Services gelten ausschließlich; entgegenstehende oder von unseren Bedingungen abweichende Bedingungen des Kunden erkennen wir nicht an, es sei denn, wir hätten ausdrücklich schriftlich ihrer Geltung zugestimmt. Unsere AVB gelten auch dann, wenn wir in Kenntnis entgegenstehender oder von unseren AVB abweichender Bedingungen des Bestellers die Lieferung an diesen vorbehaltlos ausführen. Unsere AVB gelten nur gegenüber Unternehmern im Sinn von § 310 Abs. 1 BGB sowie juristischen Personen des öffentlichen Rechts oder öffentlich-rechtliches Sondervermögen. Nebenabreden, Vorbehalte, Änderungen, Ergänzungen usw. bedürfen zu ihrer Wirksamkeit unserer schriftlichen Bestätigung. -Hinweise auf die Geltung gesetzlicher Vorschriften haben nur klarstellende Bedeutung. Auch ohne eine derartige Klarstellung gelten daher die gesetzlichen Vorschriften, soweit sie in diesen L&Z nicht unmittelbar abgeändert oder ausdrücklich ausgeschlossen werden. Bezüglich Beratungsleistungen weisen wir ausdrücklich auf Punkt 17 hin. +Hinweise auf die Geltung gesetzlicher Vorschriften haben nur klarstellende Bedeutung. Auch ohne eine derartige Klarstellung gelten daher die gesetzlichen Vorschriften, soweit sie in diesen AVB nicht unmittelbar abgeändert oder ausdrücklich ausgeschlossen werden. Bezüglich Beratungsleistungen weisen wir ausdrücklich auf Punkt 17 hin. 2. Angebote Sofern nicht ausdrücklich als bindend bezeichnet, sind unsere Angebote freibleibend; die Bestellung des Kunden ist als Angebot gemäß § 145 BGB zu qualifizieren. @@ -73,7 +73,7 @@ Sollte es bei einer Mängelrüge zu unterschiedlichen Meinungen bezüglich des K Wir haften unbeschränkt nur für Vorsatz und grobe Fahrlässigkeit sowie für Schäden aus einer Verletzung von Leben, Körper oder Gesundheit, die auf mindestens fahrlässiger Pflichtverletzung unsererseits oder unserer gesetzlichen Vertreter oder Erfüllungsgehilfen beruhen; ebenso haften wir unbeschränkt im Fall von uns übernommenen bzw. abgegebenen Garantien und Zusicherungen, sofern ein davon umfasster Mangel unsere Haftung auslöst sowie im Fall einer Haftung nach dem Produkthaftungsgesetz oder sonstigen Gefährdungshaftungstatbeständen. Im Fall sonstiger schuldhafter Verletzung wesentlicher Vertragspflichten („Kardinalpflichten“) ist unsere verbleibende Haftung auf den vertragstypischen vorhersehbaren Schaden beschränkt. Mangelfolgeschäden sowie entgangener Gewinn schließen wir grundsätzlich aus. 16. Kabeltrommeln - Unsere Kabel werden auf stabilen Vollholztrommeln geliefert. Bei Lagerzeiten über 3 Monaten ist zu beachten, dass die Trommel regelmäßig bewegt wird, um eine einseitige Belastung zu vermeiden. Folgeschäden bei Nichtbeachtung wie Instabilität schließen wir aus. Zusätzlich die [PDF zum Download](/assets/AGB MB Grid 3-2026.pdf). Auf Wunsch vermitteln wir Ihnen Partner, die diese Trommeln gegen eine Gebühr abholen. + Unsere Kabel werden auf stabilen Vollholztrommeln geliefert. Bei Lagerzeiten über 3 Monaten ist zu beachten, dass die Trommel regelmäßig bewegt wird, um eine einseitige Belastung zu vermeiden. Folgeschäden bei Nichtbeachtung wie Instabilität schließen wir aus. Zusätzlich die [PDF zum Download](/assets/AVB MB Grid 4-2026.pdf). Auf Wunsch vermitteln wir Ihnen Partner, die diese Trommeln gegen eine Gebühr abholen. 17. Technische Beratungsdienstleistungen Die technische Unterstützung ersetzt weder die Fachplanung noch die Ausführungs- oder Prüfverantwortung des beauftragten Ingenieurbüros, Planers oder der ausführenden Fachfirma bzw. verantwortlichen Abteilung. @@ -83,6 +83,6 @@ Alle Hinweise, Einschätzungen und Empfehlungen der MB Grid Solutions and Servic 18. Sonstiges Es gilt ausschließlich das Recht der Bundesrepublik Deutschland unter Ausschluss des UN-Kaufrechts (CISG). Gerichtsstand ist nach unserer Wahl Stuttgart, der Erfüllungsort der Lieferverpflichtung oder das für den Sitz des Bestellers zuständige Gericht, sofern der Besteller Kaufmann, juristische Person des öffentlichen Rechts oder öffentlich-rechtliches Sondervermögen ist oder keinen allgemeinen Gerichtsstand im Inland hat. -Mit der Veröffentlichung der vorliegenden L&Z im Internet werden alle von uns früher verwendeten Bedingungen gegenstandslos. +Mit der Veröffentlichung der vorliegenden AVB im Internet werden alle von uns früher verwendeten Bedingungen gegenstandslos. Remshalden, 22.1.2026 diff --git a/messages/de.json b/messages/de.json index f6cc0b7..ecacfdf 100644 --- a/messages/de.json +++ b/messages/de.json @@ -81,7 +81,7 @@ "legal": "Rechtliches", "impressum": "Impressum", "datenschutz": "Datenschutz", - "agb": "AGB", + "agb": "AVB", "rights": "Alle Rechte vorbehalten.", "madeWith": "Entwickelt mit", "precision": "Präzision", diff --git a/package.json b/package.json index 7c2df6f..2f22bd6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mb-grid-solutions.com", - "version": "1.0.0", + "version": "1.3.13", "type": "module", "packageManager": "pnpm@10.18.3", "scripts": { @@ -17,6 +17,9 @@ "check:http": "tsx ./scripts/check-http.ts", "check:apis": "tsx ./scripts/check-apis.ts", "check:locale": "tsx ./scripts/check-locale.ts", + "check:html": "tsx ./scripts/check-html.ts", + "check:assets": "tsx ./scripts/check-broken-assets.ts", + "check:wcag": "tsx ./scripts/wcag-sitemap.ts", "backup:db": "bash ./scripts/backup-db.sh" }, "keywords": [], @@ -72,12 +75,15 @@ "eslint": "^8.57.1", "eslint-config-next": "15.1.6", "happy-dom": "^20.6.1", + "html-validate": "^10.8.0", "husky": "^9.1.7", "jsdom": "^27.4.0", "lint-staged": "^16.2.7", + "pa11y-ci": "^4.0.1", "pino-pretty": "^13.1.3", "postcss": "^8.5.6", "prettier": "^3.5.0", + "puppeteer": "^24.37.3", "tailwindcss": "^4.1.18", "tsx": "^4.21.0", "typescript": "^5.9.3", diff --git a/public/assets/AVB MB Grid 4-2026.pdf b/public/assets/AVB MB Grid 4-2026.pdf new file mode 100644 index 0000000..92bded9 Binary files /dev/null and b/public/assets/AVB MB Grid 4-2026.pdf differ diff --git a/scripts/check-broken-assets.ts b/scripts/check-broken-assets.ts new file mode 100644 index 0000000..1b394f2 --- /dev/null +++ b/scripts/check-broken-assets.ts @@ -0,0 +1,197 @@ +import puppeteer, { HTTPResponse } from 'puppeteer'; +import axios from 'axios'; +import * as cheerio from 'cheerio'; + +const targetUrl = + process.argv.find((arg) => !arg.startsWith('--') && arg.startsWith('http')) || + process.env.NEXT_PUBLIC_BASE_URL || + 'http://localhost:3000'; +const limit = process.env.ASSET_CHECK_LIMIT ? parseInt(process.env.ASSET_CHECK_LIMIT) : 20; +const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'mbgrid'; + +async function main() { + console.log(`\n🚀 Starting Strict Asset Integrity Check for: ${targetUrl}`); + console.log(`📊 Limit: ${limit} pages\n`); + + // 1. Fetch Sitemap to discover all routes + const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`; + let urls: string[] = []; + + try { + console.log(`📥 Fetching sitemap from ${sitemapUrl}...`); + const response = await axios.get(sitemapUrl, { + headers: { Cookie: `mb_gatekeeper_session=${gatekeeperPassword}` }, + }); + + const $ = cheerio.load(response.data, { xmlMode: true }); + urls = $('url loc') + .map((i, el) => $(el).text()) + .get(); + + // Normalize to target URL instance + const urlPattern = /https?:\/\/[^\/]+/; + urls = [...new Set(urls)] + .filter((u) => u.startsWith('http')) + .map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, ''))) + .sort(); + + console.log(`✅ Found ${urls.length} target URLs.`); + + if (urls.length > limit) { + console.log( + `⚠️ Too many pages (${urls.length}). Limiting to ${limit} representative pages.`, + ); + // Simplify selection: home pages + a slice of the rest + const homeDE = urls.filter((u) => u.endsWith('/de') || u === targetUrl); + const homeEN = urls.filter((u) => u.endsWith('/en')); + const others = urls.filter((u) => !homeEN.includes(u) && !homeDE.includes(u)); + urls = [...homeDE, ...homeEN, ...others.slice(0, limit - (homeEN.length + homeDE.length))]; + } + } catch (err: any) { + console.error(`❌ Failed to fetch sitemap: ${err.message}`); + process.exit(1); + } + + // 2. Launch Headless Browser + console.log(`\n🕷️ Launching Puppeteer Headless Engine...`); + const browser = await puppeteer.launch({ + headless: true, + executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROME_PATH || undefined, + args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'], + }); + + const page = await browser.newPage(); + + // Inject Gatekeeper session bypassing auth screens + await page.setCookie({ + name: 'mb_gatekeeper_session', + value: gatekeeperPassword, + domain: new URL(targetUrl).hostname, + path: '/', + httpOnly: true, + secure: targetUrl.startsWith('https://'), + }); + + let hasBrokenAssets = false; + let hasConsoleErrors = false; + const brokenAssetsList: Array<{ url: string; status: number; page: string }> = []; + const consoleErrorsList: Array<{ type: string; error: string; page: string }> = []; + + // Listen for unhandled exceptions natively in the page + page.on('pageerror', (err: any) => { + consoleErrorsList.push({ + type: 'PAGE_ERROR', + error: err.message, + page: page.url(), + }); + hasConsoleErrors = true; + }); + + // Listen for console.error and console.warn messages (like Next.js Image warnings, hydration errors, CSP blocks) + page.on('console', (msg) => { + const type = msg.type(); + if (type === 'error' || type === 'warn') { + const text = msg.text(); + + // Exclude common browser extension noise or third party tracker warnings + if ( + text.includes('google-analytics') || + text.includes('googletagmanager') || + text.includes('SES Removing unpermitted intrinsics') || + text.includes('Third-party cookie will be blocked') || + text.includes('Fast Refresh') + ) + return; + + consoleErrorsList.push({ + type: type.toUpperCase(), + error: text, + page: page.url(), + }); + hasConsoleErrors = true; + } + }); + + // Listen to ALL network responses + page.on('response', (response: HTTPResponse) => { + const status = response.status(); + // Catch classic 404s and 500s on ANY fetch/image/script + if ( + status >= 400 && + status !== 999 && + !response.url().includes('google-analytics') && + !response.url().includes('googletagmanager') + ) { + const type = response.request().resourceType(); + + // We explicitly care about images, stylesheets, scripts, and fetch requests (API) getting 404/500s. + if (['image', 'script', 'stylesheet', 'fetch', 'xhr'].includes(type)) { + brokenAssetsList.push({ + url: response.url(), + status: status, + page: page.url(), + }); + hasBrokenAssets = true; + } + } + }); + + // 3. Scan each page + for (let i = 0; i < urls.length; i++) { + const u = urls[i]; + console.log(`[${i + 1}/${urls.length}] Scanning: ${u}`); + try { + // Wait until network is idle to ensure all Next.js hydration and image lazy-loads trigger + await page.goto(u, { waitUntil: 'networkidle0', timeout: 30000 }); + + // Force scroll to bottom to trigger any IntersectionObserver lazy-loaded images + await page.evaluate(async () => { + await new Promise