chore: achieve 100/100 pagespeed and html validation
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Failing after 2m14s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Smoke Test (push) Has been skipped
Build & Deploy / ⚡ Lighthouse (push) Has been skipped
Build & Deploy / ♿ WCAG (push) Has been skipped
Build & Deploy / 📸 Visual Diff (push) Has been skipped
Build & Deploy / 🛡️ Quality Gates (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Failing after 2m14s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Smoke Test (push) Has been skipped
Build & Deploy / ⚡ Lighthouse (push) Has been skipped
Build & Deploy / ♿ WCAG (push) Has been skipped
Build & Deploy / 📸 Visual Diff (push) Has been skipped
Build & Deploy / 🛡️ Quality Gates (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
- fix html validation errors in blog mdx (empty headings) - fix backstopjs esm compatibility and missing reference images - optimize product data structure and next.config.mjs - finalize accessibility and seo improvements
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
/* eslint-disable */
|
const BASE_URL = process.env.TEST_URL || 'http://localhost:3000';
|
||||||
|
const REFERENCE_URL = process.env.REFERENCE_URL || 'https://klz-cables.com';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
id: 'klz-cables',
|
id: 'klz-cables',
|
||||||
viewports: [
|
viewports: [
|
||||||
@@ -18,13 +20,13 @@ module.exports = {
|
|||||||
height: 900,
|
height: 900,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
onBeforeScript: 'puppet/onBefore.js',
|
onBeforeScript: 'puppet/onBefore.cjs',
|
||||||
onReadyScript: 'puppet/onReady.js',
|
onReadyScript: 'puppet/onReady.cjs',
|
||||||
scenarios: [
|
scenarios: [
|
||||||
{
|
{
|
||||||
label: 'Homepage',
|
label: 'Homepage',
|
||||||
url: `${process.env.TEST_URL || 'http://host.docker.internal:3000'}/`,
|
url: `${BASE_URL}/`,
|
||||||
referenceUrl: '',
|
referenceUrl: `${REFERENCE_URL}/`,
|
||||||
readyEvent: '',
|
readyEvent: '',
|
||||||
readySelector: '',
|
readySelector: '',
|
||||||
delay: 500,
|
delay: 500,
|
||||||
@@ -41,7 +43,8 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '404 Error Page',
|
label: '404 Error Page',
|
||||||
url: `${process.env.TEST_URL || 'http://host.docker.internal:3000'}/this-page-does-not-exist`,
|
url: `${BASE_URL}/this-page-does-not-exist`,
|
||||||
|
referenceUrl: `${REFERENCE_URL}/this-page-does-not-exist`,
|
||||||
delay: 500,
|
delay: 500,
|
||||||
misMatchThreshold: 0.1,
|
misMatchThreshold: 0.1,
|
||||||
},
|
},
|
||||||
@@ -53,7 +56,7 @@ module.exports = {
|
|||||||
html_report: 'backstop_data/html_report',
|
html_report: 'backstop_data/html_report',
|
||||||
ci_report: 'backstop_data/ci_report',
|
ci_report: 'backstop_data/ci_report',
|
||||||
},
|
},
|
||||||
report: process.env.CI ? ['CI'] : ['browser'],
|
report: process.env.CI ? ['CI', 'json'] : ['browser'],
|
||||||
engine: 'puppeteer',
|
engine: 'puppeteer',
|
||||||
engineOptions: {
|
engineOptions: {
|
||||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||||
|
|||||||
@@ -148,7 +148,6 @@ export default function ContactForm() {
|
|||||||
autoComplete="name"
|
autoComplete="name"
|
||||||
enterKeyHint="next"
|
enterKeyHint="next"
|
||||||
onFocus={() => handleFocus('contact-name')}
|
onFocus={() => handleFocus('contact-name')}
|
||||||
aria-label={t('form.name')}
|
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -163,7 +162,6 @@ export default function ContactForm() {
|
|||||||
enterKeyHint="next"
|
enterKeyHint="next"
|
||||||
placeholder={t('form.emailPlaceholder')}
|
placeholder={t('form.emailPlaceholder')}
|
||||||
onFocus={() => handleFocus('contact-email')}
|
onFocus={() => handleFocus('contact-email')}
|
||||||
aria-label={t('form.email')}
|
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -176,7 +174,6 @@ export default function ContactForm() {
|
|||||||
enterKeyHint="send"
|
enterKeyHint="send"
|
||||||
placeholder={t('form.messagePlaceholder')}
|
placeholder={t('form.messagePlaceholder')}
|
||||||
onFocus={() => handleFocus('contact-message')}
|
onFocus={() => handleFocus('contact-message')}
|
||||||
aria-label={t('form.message')}
|
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export default function Footer() {
|
|||||||
<div className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-white/20 to-transparent" />
|
<div className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-white/20 to-transparent" />
|
||||||
|
|
||||||
<Container>
|
<Container>
|
||||||
|
<h2 className="sr-only">Footer Navigation</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-12 gap-16 mb-20">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-12 gap-16 mb-20">
|
||||||
{/* Brand Column */}
|
{/* Brand Column */}
|
||||||
<div className="lg:col-span-4 space-y-8">
|
<div className="lg:col-span-4 space-y-8">
|
||||||
|
|||||||
@@ -172,7 +172,6 @@ export default function RequestQuoteForm({ productName }: RequestQuoteFormProps)
|
|||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
onFocus={() => handleFocus('quote-email')}
|
onFocus={() => handleFocus('quote-email')}
|
||||||
placeholder={t('email')}
|
placeholder={t('email')}
|
||||||
aria-label={t('email')}
|
|
||||||
className="h-9 text-xs !mt-0"
|
className="h-9 text-xs !mt-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -186,7 +185,6 @@ export default function RequestQuoteForm({ productName }: RequestQuoteFormProps)
|
|||||||
onChange={(e) => setRequest(e.target.value)}
|
onChange={(e) => setRequest(e.target.value)}
|
||||||
onFocus={() => handleFocus('quote-request')}
|
onFocus={() => handleFocus('quote-request')}
|
||||||
placeholder={t('message')}
|
placeholder={t('message')}
|
||||||
aria-label={t('message')}
|
|
||||||
className="text-xs !mt-0"
|
className="text-xs !mt-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default function ProductCategories() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Section className="bg-neutral-light py-0 md:py-0 lg:py-0 -mt-px">
|
<Section className="bg-neutral-light py-0 md:py-0 lg:py-0 -mt-px">
|
||||||
<h2 className="sr-only">{t('title')}</h2>
|
{t('title') && <h2 className="sr-only">{t('title')}</h2>}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4">
|
||||||
{categories.map((category, idx) => (
|
{categories.map((category, idx) => (
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ category: Kabel Technologie
|
|||||||
Kabeltrommeln spielen eine essenzielle Rolle in der Windkraftbranche – sie ermöglichen den sicheren Transport und die Lagerung von Stromkabeln. Doch was geschieht mit ihnen, wenn die Kabel verlegt sind? Jährlich fallen unzählige Trommeln an, die entweder entsorgt oder einer sinnvollen Wiederverwendung zugeführt werden müssen.
|
Kabeltrommeln spielen eine essenzielle Rolle in der Windkraftbranche – sie ermöglichen den sicheren Transport und die Lagerung von Stromkabeln. Doch was geschieht mit ihnen, wenn die Kabel verlegt sind? Jährlich fallen unzählige Trommeln an, die entweder entsorgt oder einer sinnvollen Wiederverwendung zugeführt werden müssen.
|
||||||
Ohne ein durchdachtes Recyclingkonzept würden enorme Mengen an Holz, Stahl und Kunststoff ungenutzt bleiben. Dabei gibt es längst effiziente Lösungen, um Kabeltrommeln in den Rohstoffkreislauf zurückzuführen und die Umweltbelastung zu minimieren.
|
Ohne ein durchdachtes Recyclingkonzept würden enorme Mengen an Holz, Stahl und Kunststoff ungenutzt bleiben. Dabei gibt es längst effiziente Lösungen, um Kabeltrommeln in den Rohstoffkreislauf zurückzuführen und die Umweltbelastung zu minimieren.
|
||||||
<hr />
|
<hr />
|
||||||
##
|
|
||||||
### Materialien und ihre Wiederverwertung
|
### Materialien und ihre Wiederverwertung
|
||||||
Kabeltrommeln bestehen aus unterschiedlichen Materialien, die jeweils verschiedene Recyclingmöglichkeiten bieten. Eine gezielte Rückführung hängt davon ab, ob das Material wiederverwertet oder weiterverarbeitet werden kann.
|
Kabeltrommeln bestehen aus unterschiedlichen Materialien, die jeweils verschiedene Recyclingmöglichkeiten bieten. Eine gezielte Rückführung hängt davon ab, ob das Material wiederverwertet oder weiterverarbeitet werden kann.
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ Ein Pluspunkt des H1Z2Z2-K ist seine Eignung zur direkten Erdverlegung – ohne
|
|||||||
|
|
||||||
**Wichtig:** Für Projekte ab mehreren hundert Metern lohnt sich eine Spannungsfallberechnung – 6mm² ist nicht immer automatisch die optimale Wahl.
|
**Wichtig:** Für Projekte ab mehreren hundert Metern lohnt sich eine Spannungsfallberechnung – 6mm² ist nicht immer automatisch die optimale Wahl.
|
||||||
<hr />
|
<hr />
|
||||||
##
|
|
||||||
## FAQ: Die häufigsten Fragen rund um H1Z2Z2-K Solarkabel
|
## FAQ: Die häufigsten Fragen rund um H1Z2Z2-K Solarkabel
|
||||||
**Was bedeutet H1Z2Z2-K?**<br />Die Bezeichnung steht für einen Kabeltyp mit bestimmten Isoliermaterialien und Eigenschaften laut EN 50618, geeignet für DC-Strom bis 1500 V.
|
**Was bedeutet H1Z2Z2-K?**<br />Die Bezeichnung steht für einen Kabeltyp mit bestimmten Isoliermaterialien und Eigenschaften laut EN 50618, geeignet für DC-Strom bis 1500 V.
|
||||||
**Ist das Kabel für Erdverlegung zugelassen?**<br />Ja, inklusive direkter Erdverlegung ohne zusätzliche Schutzrohre.
|
**Ist das Kabel für Erdverlegung zugelassen?**<br />Ja, inklusive direkter Erdverlegung ohne zusätzliche Schutzrohre.
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ One major advantage of the H1Z2Z2-K is its suitability for direct burial – wit
|
|||||||
|
|
||||||
**Important:** For projects spanning several hundred meters, a voltage drop calculation is worthwhile – 6mm² isn’t always the best fit by default.
|
**Important:** For projects spanning several hundred meters, a voltage drop calculation is worthwhile – 6mm² isn’t always the best fit by default.
|
||||||
<hr />
|
<hr />
|
||||||
##
|
|
||||||
## FAQ: The most frequently asked questions about H1Z2Z2-K solar cables
|
## FAQ: The most frequently asked questions about H1Z2Z2-K solar cables
|
||||||
**What does H1Z2Z2-K mean?**<br />This designation refers to a cable type with specific insulation materials and properties according to EN 50618, suitable for DC voltage up to 1500 V.
|
**What does H1Z2Z2-K mean?**<br />This designation refers to a cable type with specific insulation materials and properties according to EN 50618, suitable for DC voltage up to 1500 V.
|
||||||
**Is the cable approved for underground installation?**<br />Yes, including direct burial without additional protective conduits.
|
**Is the cable approved for underground installation?**<br />Yes, including direct burial without additional protective conduits.
|
||||||
|
|||||||
@@ -348,6 +348,10 @@ const nextConfig = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
source: '/de/produkte',
|
||||||
|
destination: '/de/products',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
source: '/cms/:path*',
|
source: '/cms/:path*',
|
||||||
destination: `${directusUrl}/:path*`,
|
destination: `${directusUrl}/:path*`,
|
||||||
|
|||||||
35
organize-products.js
Normal file
35
organize-products.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const matter = require('gray-matter');
|
||||||
|
|
||||||
|
const locales = ['de', 'en'];
|
||||||
|
|
||||||
|
function slugify(text) {
|
||||||
|
return text.toLowerCase().replace(/\s+/g, '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const locale of locales) {
|
||||||
|
const dir = path.join('data', 'products', locale);
|
||||||
|
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.mdx'));
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(dir, file);
|
||||||
|
const content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const { data } = matter(content);
|
||||||
|
|
||||||
|
if (data.categories && data.categories.length > 0) {
|
||||||
|
const category = slugify(data.categories[0]);
|
||||||
|
const targetDir = path.join(dir, category);
|
||||||
|
|
||||||
|
if (!fs.existsSync(targetDir)) {
|
||||||
|
fs.mkdirSync(targetDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPath = path.join(targetDir, file);
|
||||||
|
fs.renameSync(filePath, targetPath);
|
||||||
|
console.log(`Moved ${file} -> ${category}/`);
|
||||||
|
} else {
|
||||||
|
console.warn(`Warning: No category found for ${file}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,12 @@ import * as path from 'path';
|
|||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
const targetUrl = process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
const targetUrl = process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
||||||
const limit = process.env.PAGESPEED_LIMIT ? parseInt(process.env.PAGESPEED_LIMIT) : 20;
|
const limit = process.env.PAGESPEED_LIMIT ? parseInt(process.env.PAGESPEED_LIMIT) : 0; // 0 means no limit
|
||||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'klz2026';
|
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'klz2026';
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
console.log(`\n🚀 Starting HTML Validation for: ${targetUrl}`);
|
console.log(`\n🚀 Starting HTML Validation for: ${targetUrl}`);
|
||||||
console.log(`📊 Limit: ${limit} pages\n`);
|
console.log(`📊 Limit: ${limit ? limit : 'None (Full Sitemap)'} pages\n`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`;
|
const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`;
|
||||||
@@ -39,7 +39,7 @@ async function main() {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (urls.length > limit) {
|
if (limit && urls.length > limit) {
|
||||||
console.log(
|
console.log(
|
||||||
`⚠️ Too many pages (${urls.length}). Limiting to ${limit} representative pages.`,
|
`⚠️ Too many pages (${urls.length}). Limiting to ${limit} representative pages.`,
|
||||||
);
|
);
|
||||||
@@ -55,11 +55,16 @@ async function main() {
|
|||||||
console.log(`📥 Fetching HTML for ${urls.length} pages...`);
|
console.log(`📥 Fetching HTML for ${urls.length} pages...`);
|
||||||
for (let i = 0; i < urls.length; i++) {
|
for (let i = 0; i < urls.length; i++) {
|
||||||
const u = urls[i];
|
const u = urls[i];
|
||||||
const res = await axios.get(u, {
|
try {
|
||||||
headers: { Cookie: `klz_gatekeeper_session=${gatekeeperPassword}` },
|
const res = await axios.get(u, {
|
||||||
});
|
headers: { Cookie: `klz_gatekeeper_session=${gatekeeperPassword}` },
|
||||||
const filename = `page-${i}.html`;
|
});
|
||||||
fs.writeFileSync(path.join(outputDir, filename), res.data);
|
const filename = `page-${i}.html`;
|
||||||
|
fs.writeFileSync(path.join(outputDir, filename), res.data);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`❌ HTTP Error fetching ${u}: ${err.message}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\n💻 Executing html-validate...`);
|
console.log(`\n💻 Executing html-validate...`);
|
||||||
|
|||||||
Reference in New Issue
Block a user