Compare commits

...

157 Commits

Author SHA1 Message Date
cf7af73b72 feat: Automate Directus CMS bootstrapping in the dev script and update gitignore rules.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 20s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m15s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 3m32s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Failing after 1m0s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-01 12:47:46 +01:00
d526bfe56f chore: Remove various unused uploaded files from Directus. 2026-02-01 12:47:30 +01:00
4cb7d438a0 feat: make Directus CMS URL configurable via environment variable with a default.
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m17s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 3m42s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 56s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-01 11:17:24 +01:00
03e597442b feat: Centralize OG image font loading and sizing, simplify product page OG generation, and refine template styling.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m36s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Failing after 1m31s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 3s
2026-02-01 11:05:37 +01:00
5f9ee7d976 chore: resolve merge conflict in deploy workflow and sync docker-compose
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 24s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Failing after 1m16s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Has been skipped
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-01 02:18:25 +01:00
a4ea42a043 directus 2026-02-01 02:10:24 +01:00
ee04d2422c directus 2026-02-01 02:02:03 +01:00
26fc34299e directus 2026-02-01 00:58:54 +01:00
6d13611a16 directus 2026-02-01 00:55:33 +01:00
4a9246be5e directus 2026-01-31 23:52:51 +01:00
2ed038174d directus 2026-01-31 23:32:01 +01:00
c1304403a1 ci cd
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 22s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Failing after 1m34s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Has been skipped
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 1s
2026-01-31 22:32:32 +01:00
5036c5fe28 deploy
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 4m13s
2026-01-31 21:10:12 +01:00
50a524c515 gitea
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 3m54s
2026-01-31 19:21:53 +01:00
57886a01d6 traefik
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m58s
2026-01-31 18:36:34 +01:00
c89bd8e80f staging deploy
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m54s
2026-01-31 18:07:28 +01:00
9c54322654 datasheet download
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 4m1s
2026-01-31 10:40:17 +01:00
8a80eb7b9a og
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m55s
2026-01-31 10:35:07 +01:00
c1773a7072 datasheets as downloads
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m59s
2026-01-31 10:29:39 +01:00
33ed13d255 og
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 3m28s
2026-01-31 10:25:25 +01:00
0f5811edb9 og
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 1m47s
2026-01-31 10:21:24 +01:00
06bbed8c21 strapi 2026-01-31 10:11:45 +01:00
f5a879fa60 Merge branch 'main' into feature/strapi 2026-01-30 22:10:16 +01:00
e4eabd7a86 sheets
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 17m33s
2026-01-30 22:10:01 +01:00
757df76f36 terms
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 4m1s
2026-01-30 19:31:22 +01:00
14b2f83971 sheets 2026-01-30 00:17:46 +01:00
51565fdf41 sheets 2026-01-29 22:51:56 +01:00
be9f9cf483 strapi 2026-01-29 19:47:55 +01:00
506c8682fe umami
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m52s
2026-01-29 17:43:06 +01:00
a909de30f3 filter products
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m52s
2026-01-29 17:34:15 +01:00
a2f94f15bc og
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 4m48s
2026-01-29 17:26:02 +01:00
13e56a88bc headings
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 4m3s
2026-01-29 16:51:11 +01:00
bb7d17001b og
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m57s
2026-01-29 16:33:04 +01:00
920efa0083 pdf sheets 2026-01-29 16:27:09 +01:00
0b81d1a4cb og
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 14s
2026-01-29 16:18:37 +01:00
1d5bdeba26 spacing 2026-01-29 15:59:53 +01:00
a0c3fbbc7e application field to mdx 2026-01-29 15:45:35 +01:00
8101a9f156 reveal
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m57s
2026-01-29 15:11:04 +01:00
7b6f4b5ea4 deploy
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 4m2s
2026-01-29 14:47:43 +01:00
658057cdb1 deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 5m55s
2026-01-29 13:57:35 +01:00
2aa5d5b00e deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 3m53s
2026-01-29 13:52:37 +01:00
7f2f6f5aca deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 5m48s
2026-01-29 11:33:30 +01:00
4e50482769 remove redis
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 5m50s
2026-01-29 02:23:41 +01:00
1da1f05cdd remove varnish
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 4m55s
2026-01-29 02:12:39 +01:00
15cfb314b1 fixes
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 3m53s
2026-01-29 01:05:57 +01:00
a3da6192e3 remove smoke test 2026-01-29 00:34:41 +01:00
af33c6225d deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 5m32s
2026-01-29 00:09:15 +01:00
9ee09bbe4b deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 4m27s
2026-01-28 23:46:51 +01:00
3f0858a1ba smoke tests
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 4m17s
2026-01-28 21:39:16 +01:00
a85fe64ccb build
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 4m14s
2026-01-28 19:41:01 +01:00
21b16a5e6c deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 1m51s
2026-01-28 19:05:20 +01:00
6115e0e0d4 deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 1m51s
2026-01-28 17:27:43 +01:00
859d034ed7 env
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 1m17s
2026-01-28 15:26:36 +01:00
91ebc54571 env
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 4m12s
2026-01-28 15:14:01 +01:00
d6c1d6bae6 env
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 4m2s
2026-01-28 15:06:21 +01:00
407b2227b3 env
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m53s
2026-01-28 01:00:30 +01:00
2896556659 env
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m52s
2026-01-28 00:41:29 +01:00
8242687b07 env
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m46s
2026-01-28 00:34:40 +01:00
dab4f3f5b5 logging 2026-01-28 00:13:19 +01:00
b18ee8d7a0 envs
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m52s
2026-01-28 00:02:24 +01:00
cbca29cbcf env
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m52s
2026-01-27 23:51:48 +01:00
5a5c10ca36 env
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 1m46s
2026-01-27 23:43:14 +01:00
ad6bfe1457 404
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m48s
2026-01-27 23:36:12 +01:00
b5c3fc6649 404
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m43s
2026-01-27 23:09:56 +01:00
03609f113d 404
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 2m31s
2026-01-27 22:38:05 +01:00
622180c483 404s
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 1m40s
2026-01-27 22:27:04 +01:00
41865ab9ab deploy
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m45s
2026-01-27 20:32:32 +01:00
986fcd067e deploy
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m40s
2026-01-27 19:09:43 +01:00
d27616ed43 deploy
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m46s
2026-01-27 18:46:02 +01:00
b9a3e47662 deploy
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m43s
2026-01-27 18:30:54 +01:00
a5e34053f7 gotify 2026-01-27 18:22:55 +01:00
e6f9ad36d3 deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 3m33s
2026-01-27 18:16:26 +01:00
2e0456b081 gotify
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 11s
2026-01-27 17:55:38 +01:00
ad37a372a7 deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 10s
2026-01-27 12:37:49 +01:00
8490b691e1 contact 2026-01-27 12:36:00 +01:00
334c76935e env
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m39s
2026-01-27 11:32:01 +01:00
6624cfc3ad sentry
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m37s
2026-01-27 10:31:36 +01:00
20d7d8405a toc
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m39s
2026-01-27 10:26:17 +01:00
12501ea51a fixes 2026-01-27 10:12:36 +01:00
70f1813e33 fix 404
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m38s
2026-01-27 09:23:15 +01:00
69e39b06cf fix redirects
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m39s
2026-01-27 09:17:55 +01:00
3b5174cd12 lightbox
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m46s
2026-01-27 08:57:43 +01:00
875cf1bd07 buttons 2026-01-27 08:47:52 +01:00
e284bb94af hero
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m39s
2026-01-27 08:30:18 +01:00
8eea94ceda hero
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m35s
2026-01-27 08:21:55 +01:00
05b10018a6 hero center
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m39s
2026-01-27 08:07:25 +01:00
3b493abb3d env
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m34s
2026-01-27 00:50:52 +01:00
f54d8277b3 hero on mobile 2026-01-27 00:49:19 +01:00
5c71e9a064 env
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m36s
2026-01-27 00:30:12 +01:00
3cab376cd1 lightbox
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m31s
2026-01-27 00:21:03 +01:00
3c45e5563e build
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m34s
2026-01-27 00:14:10 +01:00
13ab4bde75 og image
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 13s
2026-01-27 00:10:10 +01:00
a805c7b8de titles
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Has been cancelled
2026-01-27 00:07:00 +01:00
b8fdbfb10b lightbox
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m39s
2026-01-26 23:59:10 +01:00
081adb02be fix
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 13s
2026-01-26 23:45:37 +01:00
3f17d08b04 deploy
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m40s
2026-01-26 23:28:23 +01:00
d40f4544ea deploy
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m30s
2026-01-26 23:12:22 +01:00
041b5534c9 build
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m37s
2026-01-26 23:00:45 +01:00
dea3b57627 forms 2026-01-26 22:55:13 +01:00
baec7cc94a reveals
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 2m4s
2026-01-26 22:51:11 +01:00
b43b1c4314 fix 2026-01-26 22:19:13 +01:00
615757963c hero entrance 2026-01-26 22:04:49 +01:00
0094871358 ssr 2026-01-26 22:00:48 +01:00
8fef3b6c7f fix 2026-01-26 21:52:30 +01:00
cbb7855804 frontend fixes
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 1m37s
2026-01-26 20:21:17 +01:00
a8f7c5370b colors
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 11s
2026-01-26 17:42:43 +01:00
f459bf230d colors
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Has been cancelled
2026-01-26 17:40:33 +01:00
570a4977dd deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Has been cancelled
2026-01-26 17:38:24 +01:00
da04c108ef deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Has been cancelled
2026-01-26 17:37:25 +01:00
cb207d6a01 deploy
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 10s
2026-01-26 16:57:20 +01:00
6dc0ff4644 deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 3m33s
2026-01-26 15:56:29 +01:00
e648d30767 deploy
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m30s
2026-01-26 15:36:46 +01:00
9c26ddddbf deploy
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m28s
2026-01-26 14:08:17 +01:00
61b4b37111 build
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m29s
2026-01-26 13:38:48 +01:00
feedf30be1 env vars
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 3m30s
2026-01-26 12:57:06 +01:00
b596c22011 logs 2026-01-26 12:15:34 +01:00
377f583ff1 deploy
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m29s
2026-01-26 11:08:51 +01:00
4d72a5bf86 colors
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m35s
2026-01-26 02:39:22 +01:00
f50f41530d deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Has been cancelled
2026-01-26 02:36:58 +01:00
f8274cad1b logs 2026-01-26 02:34:54 +01:00
f2dd76a7a6 build
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m30s
2026-01-26 02:30:19 +01:00
574d5a8a9a deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 1m32s
2026-01-26 02:19:48 +01:00
ac1e22017e logging
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m46s
2026-01-26 02:02:46 +01:00
a503d3e539 colors 2026-01-26 01:59:40 +01:00
c2eeeafd56 deploy
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m43s
2026-01-26 01:45:10 +01:00
78cb7207e6 deploy 2026-01-26 01:41:07 +01:00
ca4c36ad01 deploy 2026-01-26 01:35:12 +01:00
c9dcf73021 deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 3m49s
2026-01-26 01:28:04 +01:00
8146ee78fa deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Has been cancelled
2026-01-26 01:25:52 +01:00
58878e9f64 deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 3m21s
2026-01-26 01:20:41 +01:00
f43c97e712 deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 3m23s
2026-01-26 01:14:54 +01:00
888b46eed0 deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 3m19s
2026-01-26 01:03:16 +01:00
f1c64f000c deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 3m22s
2026-01-26 00:57:43 +01:00
9b561f2176 deploy 2026-01-26 00:48:28 +01:00
f7930503d5 deploy 2026-01-26 00:34:50 +01:00
bb98014ea1 deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 4m43s
2026-01-25 23:44:44 +01:00
defde86fc9 build
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 4m45s
2026-01-25 23:20:39 +01:00
69141175cf deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 3m51s
2026-01-25 23:11:03 +01:00
4fdbc0f5cf deploy
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 17s
2026-01-25 18:07:19 +01:00
97950d574e deploy
Some checks failed
Build & Deploy KLZ Cables / deploy (push) Failing after 17s
2026-01-25 17:56:04 +01:00
c04a134eca deploy
Some checks failed
Build & Deploy KLZ Cables / deploy (push) Has been cancelled
2026-01-25 17:52:57 +01:00
3b03572fb0 deploy
Some checks failed
Build & Deploy KLZ Cables / deploy (push) Has been cancelled
2026-01-25 17:51:28 +01:00
a4c926ceb1 deploy
Some checks failed
Build & Deploy KLZ Cables / deploy (push) Failing after 28s
2026-01-25 17:40:04 +01:00
f5edc08e9d deploy 2026-01-25 17:38:56 +01:00
28a1cb4b4c deploy
Some checks failed
Build & Deploy KLZ Cables / deploy (push) Failing after 36s
2026-01-25 17:30:54 +01:00
8dcc27ffcd deploy
Some checks failed
Build & Deploy KLZ Cables / deploy (push) Failing after 36s
2026-01-25 17:23:54 +01:00
a9d256ea55 deploy
Some checks failed
Build & Deploy KLZ Cables / deploy (push) Failing after 36s
2026-01-25 17:13:55 +01:00
97b1a94012 simple deploy
Some checks failed
Build & Deploy KLZ Cables / deploy (push) Failing after 35s
2026-01-25 17:10:47 +01:00
831c8be588 deploy
Some checks failed
Build & Deploy KLZ Cables / deploy (push) Failing after 29s
2026-01-25 17:01:00 +01:00
c121398381 deploy
Some checks failed
Build & Deploy KLZ Cables / deploy (push) Has been cancelled
2026-01-25 17:00:02 +01:00
a831bed335 build
All checks were successful
Build & Deploy KLZ Cables / deploy (push) Successful in 5m45s
2026-01-25 13:53:27 +01:00
5659073f95 build
Some checks failed
Build & Deploy KLZ Cables / deploy (push) Failing after 36s
2026-01-25 13:51:08 +01:00
2d93321a91 build
Some checks failed
Build & Deploy KLZ Cables / deploy (push) Failing after 37s
2026-01-25 13:50:09 +01:00
fb6af84a42 interactive map
Some checks failed
Build & Deploy KLZ Cables / deploy (push) Failing after 39s
2026-01-25 13:46:31 +01:00
8affb7878f deploy
Some checks failed
Build & Deploy KLZ Cables / deploy (push) Has been cancelled
2026-01-25 13:44:09 +01:00
c074a5d935 logging 2026-01-25 13:42:28 +01:00
4dbf566f0c hero on mobile 2026-01-25 13:38:14 +01:00
378 changed files with 11104 additions and 2314 deletions

View File

@@ -6,5 +6,4 @@ node_modules
*.md
docs
reference
scripts
public/datasheets/*.pdf

31
.env
View File

@@ -1,20 +1,31 @@
# Application
NODE_ENV=production
NEXT_PUBLIC_BASE_URL=https://klz-cables.com
UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
NEXT_PUBLIC_UMAMI_WEBSITE_ID=59a7db94-0100-4c7e-98ef-99f45b17f9c3
SENTRY_DSN=https://c10957d0182245b1a2a806ac2d34a197@errors.infra.mintel.me/1
LOG_LEVEL=info
# WooCommerce & WordPress
WOOCOMMERCE_URL=https://klz-cables.com
WOOCOMMERCE_CONSUMER_KEY=ck_38d97df86880e8fefbd54ab5cdf47a9c5a9e5b39
WOOCOMMERCE_CONSUMER_SECRET=cs_d675ee2ac2ec7c22de84ae5451c07e42b1717759
WORDPRESS_APP_PASSWORD=DlJH 49dp fC3a Itc3 Sl7Z Wz0k'
# Umami Analytics
NEXT_PUBLIC_UMAMI_WEBSITE_ID=59a7db94-0100-4c7e-98ef-99f45b17f9c3
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
# GlitchTip (Sentry protocol)
SENTRY_DSN=https://c10957d0182245b1a2a806ac2d34a197@errors.infra.mintel.me/1
NEXT_PUBLIC_SENTRY_DSN=https://c10957d0182245b1a2a806ac2d34a197@klz-cables.com/errors/1
WORDPRESS_APP_PASSWORD="DlJH 49dp fC3a Itc3 Sl7Z Wz0k"
# SMTP Configuration
MAIL_HOST=smtp.eu.mailgun.org
MAIL_PORT=587
MAIL_USERNAME=postmaster@mg.mintel.me
MAIL_PASSWORD=4592fcb94599ee1a45b4ac2386fd0a64-102c75d8-ca2870e6
MAIL_FROM=KLZ Cables <postmaster@mg.mintel.me>
MAIL_FROM="KLZ Cables <postmaster@mg.mintel.me>"
MAIL_RECIPIENTS=marc@cablecreations.de,info@klz-cables.com
# Directus
DIRECTUS_URL=https://cms.klz-cables.com
DIRECTUS_KEY=59fb8f4c1a51b18fe28ad947f713914e
DIRECTUS_SECRET=7459038d41401dfb11254cf7f1ef2d0f
DIRECTUS_ADMIN_EMAIL=marc@mintel.me
DIRECTUS_ADMIN_PASSWORD=Tim300493.
DIRECTUS_DB_NAME=directus
DIRECTUS_DB_USER=directus
DIRECTUS_DB_PASSWORD=directus

97
.env.example Normal file
View File

@@ -0,0 +1,97 @@
# ============================================================================
# KLZ Cables - Environment Configuration
# ============================================================================
# Copy this file to .env for local development
# For production, use .env.production as a template
# ============================================================================
# ────────────────────────────────────────────────────────────────────────────
# Application Configuration
# ────────────────────────────────────────────────────────────────────────────
NODE_ENV=development
NEXT_PUBLIC_BASE_URL=http://localhost:3000
# ────────────────────────────────────────────────────────────────────────────
# Analytics (Umami)
# ────────────────────────────────────────────────────────────────────────────
# Optional: Leave empty to disable analytics
NEXT_PUBLIC_UMAMI_WEBSITE_ID=
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
# ────────────────────────────────────────────────────────────────────────────
# Error Tracking (GlitchTip/Sentry)
# ────────────────────────────────────────────────────────────────────────────
# Optional: Leave empty to disable error tracking
SENTRY_DSN=
# ────────────────────────────────────────────────────────────────────────────
# Email Configuration (SMTP)
# ────────────────────────────────────────────────────────────────────────────
# Required for contact form functionality
MAIL_HOST=smtp.eu.mailgun.org
MAIL_PORT=587
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_FROM=KLZ Cables <noreply@klz-cables.com>
MAIL_RECIPIENTS=info@klz-cables.com
# ────────────────────────────────────────────────────────────────────────────
# Logging
# ────────────────────────────────────────────────────────────────────────────
LOG_LEVEL=info
# ────────────────────────────────────────────────────────────────────────────
# Deployment Configuration (CI/CD only)
# ────────────────────────────────────────────────────────────────────────────
# These are typically set by the CI/CD workflow
IMAGE_TAG=latest
TRAEFIK_HOST=klz-cables.com
ENV_FILE=.env
# ────────────────────────────────────────────────────────────────────────────
# Varnish Cache (Docker only)
# ────────────────────────────────────────────────────────────────────────────
VARNISH_CACHE_SIZE=256m
# ────────────────────────────────────────────────────────────────────────────
# Strapi CMS
# ────────────────────────────────────────────────────────────────────────────
STRAPI_DATABASE_NAME=strapi
STRAPI_DATABASE_USERNAME=strapi
STRAPI_DATABASE_PASSWORD=strapi
STRAPI_URL=http://localhost:1337
APP_KEYS=toBeModified1,toBeModified2
API_TOKEN_SALT=tobemodified
ADMIN_JWT_SECRET=tobemodified
TRANSFER_TOKEN_SALT=tobemodified
JWT_SECRET=tobemodified
# ============================================================================
# IMPORTANT NOTES
# ============================================================================
#
# BUILD-TIME vs RUNTIME Variables:
# ─────────────────────────────────
# • NEXT_PUBLIC_* variables are baked into the client bundle at BUILD time
# They must be provided as --build-arg when building the Docker image
#
# • All other variables are used at RUNTIME only
# They are loaded from the .env file by docker-compose
#
# Docker Deployment:
# ──────────────────
# 1. Build-time: Only NEXT_PUBLIC_* vars are needed (via --build-arg)
# 2. Runtime: All vars are loaded from .env file on the server
# 3. Branch Deployments:
# - main branch uses .env.prod
# - staging branch uses .env.staging
# - CI/CD supports STAGING_ prefix for all secrets to override defaults
# - TRAEFIK_HOST is automatically derived from NEXT_PUBLIC_BASE_URL
#
# Security:
# ─────────
# • NEVER commit .env files with real credentials to git
# • Use Gitea/GitHub secrets for CI/CD workflows
# • Store production .env file securely on the server only
#
# ============================================================================

40
.env.production Normal file
View File

@@ -0,0 +1,40 @@
# ============================================================================
# KLZ Cables - Production Environment Configuration
# ============================================================================
# This file contains runtime environment variables for the production deployment.
# It should be placed on the production server at: /home/deploy/sites/klz-cables.com/.env
#
# IMPORTANT: This file contains sensitive data and should NEVER be committed to git.
# ============================================================================
# Application
NODE_ENV=production
NEXT_PUBLIC_BASE_URL=https://klz-cables.com
# Analytics (Umami)
NEXT_PUBLIC_UMAMI_WEBSITE_ID=
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
# Error Tracking (GlitchTip/Sentry)
SENTRY_DSN=
# Email Configuration (Mailgun)
MAIL_HOST=smtp.eu.mailgun.org
MAIL_PORT=587
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_FROM=KLZ Cables <noreply@klz-cables.com>
MAIL_RECIPIENTS=info@klz-cables.com
# Strapi
STRAPI_DATABASE_NAME=strapi
STRAPI_DATABASE_USERNAME=strapi
STRAPI_DATABASE_PASSWORD=
APP_KEYS=
API_TOKEN_SALT=
ADMIN_JWT_SECRET=
TRANSFER_TOKEN_SALT=
JWT_SECRET=
# Varnish Cache Size (optional)
VARNISH_CACHE_SIZE=256m

32
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,32 @@
name: CI - Lint, Typecheck & Test
on:
push:
branches-ignore:
- main
pull_request:
jobs:
quality-assurance:
runs-on: docker
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: 🔍 Lint
run: npm run lint
- name: 🏗️ Typecheck
run: npm run typecheck
- name: 🧪 Test
run: npm run test

View File

@@ -4,150 +4,294 @@ on:
push:
branches:
- main
tags:
- 'v*'
jobs:
deploy:
# ──────────────────────────────────────────────────────────────────────────────
# JOB 1: Prepare & Determine Environment
# ──────────────────────────────────────────────────────────────────────────────
prepare:
name: 🔍 Prepare Environment
runs-on: docker
outputs:
target: ${{ steps.determine.outputs.target }}
image_tag: ${{ steps.determine.outputs.image_tag }}
env_file: ${{ steps.determine.outputs.env_file }}
traefik_host: ${{ steps.determine.outputs.traefik_host }}
is_prod: ${{ steps.determine.outputs.is_prod }}
gotify_title: ${{ steps.determine.outputs.gotify_title }}
gotify_priority: ${{ steps.determine.outputs.gotify_priority }}
short_sha: ${{ steps.determine.outputs.short_sha }}
commit_msg: ${{ steps.determine.outputs.commit_msg }}
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install tools
- name: 🔍 Environment & Version ermitteln
id: determine
run: |
apt-get update
apt-get install -y \
docker.io \
openssh-client \
rsync
TAG="${{ github.ref_name }}"
SHORT_SHA="${{ github.sha }}"
SHORT_SHA="${SHORT_SHA:0:9}"
COMMIT_MSG=$(git log -1 --pretty=%s || echo "No commit message available")
- name: Login to registry
if [[ "${{ github.ref_type }}" == "branch" && "$TAG" == "main" ]]; then
TARGET="testing"
IMAGE_TAG="main-${SHORT_SHA}"
ENV_FILE=".env.testing"
TRAEFIK_HOST="\`testing.klz-cables.com\`"
IS_PROD="false"
GOTIFY_TITLE="🧪 Testing-Deploy"
GOTIFY_PRIORITY=4
elif [[ "${{ github.ref_type }}" == "tag" ]]; then
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
TARGET="production"
IMAGE_TAG="$TAG"
ENV_FILE=".env.prod"
TRAEFIK_HOST="\`klz-cables.com\`, \`www.klz-cables.com\`"
IS_PROD="true"
GOTIFY_TITLE="🚀 Production-Release"
GOTIFY_PRIORITY=6
elif [[ "$TAG" =~ -rc\. || "$TAG" =~ -beta\. || "$TAG" =~ -alpha\. ]]; then
TARGET="staging"
IMAGE_TAG="$TAG"
ENV_FILE=".env.staging"
TRAEFIK_HOST="\`staging.klz-cables.com\`"
IS_PROD="false"
GOTIFY_TITLE="🧪 Staging-Deploy (Pre-Release)"
GOTIFY_PRIORITY=5
else
TARGET="skip"
GOTIFY_TITLE="❓ Unbekannter Tag"
GOTIFY_PRIORITY=3
fi
else
TARGET="skip"
fi
echo "target=$TARGET" >> $GITHUB_OUTPUT
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
echo "env_file=$ENV_FILE" >> $GITHUB_OUTPUT
echo "traefik_host=$TRAEFIK_HOST" >> $GITHUB_OUTPUT
echo "is_prod=$IS_PROD" >> $GITHUB_OUTPUT
echo "gotify_title=$GOTIFY_TITLE" >> $GITHUB_OUTPUT
echo "gotify_priority=$GOTIFY_PRIORITY" >> $GITHUB_OUTPUT
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
echo "commit_msg=$COMMIT_MSG" >> $GITHUB_OUTPUT
# ──────────────────────────────────────────────────────────────────────────────
# JOB 2: Quality Assurance (Lint & Test)
# ──────────────────────────────────────────────────────────────────────────────
qa:
name: 🧪 Quality Assurance
needs: prepare
if: needs.prepare.outputs.target != 'skip'
runs-on: docker
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: 🔍 Lint & Typecheck
run: |
npm run lint
npm run typecheck
- name: 🧪 Test
run: npm run test
# ──────────────────────────────────────────────────────────────────────────────
# JOB 3: Build & Push Docker Image
# ──────────────────────────────────────────────────────────────────────────────
build:
name: 🏗️ Build & Push
needs: [prepare, qa]
runs-on: docker
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: 🔐 Registry Login
run: |
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
- name: 🏗️ Docker Image bauen & pushen
env:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASS: ${{ secrets.REGISTRY_PASS }}
IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }}
TARGET: ${{ needs.prepare.outputs.target }}
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_BASE_URL || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_BASE_URL || secrets.TESTING_NEXT_PUBLIC_BASE_URL || secrets.NEXT_PUBLIC_BASE_URL) }}
NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.TESTING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID) }}
NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.TESTING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL) }}
DIRECTUS_URL: ${{ needs.prepare.outputs.target == 'production' && 'https://cms.klz-cables.com' || (needs.prepare.outputs.target == 'staging' && 'https://cms-staging.klz-cables.com' || 'https://cms-testing.klz-cables.com') }}
run: |
echo "$REGISTRY_PASS" | DOCKER_API_VERSION=1.44 docker login registry.infra.mintel.me -u "$REGISTRY_USER" --password-stdin
- name: Build image
run: |
DOCKER_API_VERSION=1.44 docker build \
echo "🏗️ Building → $TARGET / $IMAGE_TAG"
docker buildx build \
--pull \
--build-arg NEXT_PUBLIC_UMAMI_WEBSITE_ID=${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID }} \
--build-arg NEXT_PUBLIC_UMAMI_SCRIPT_URL=${{ secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL }} \
--build-arg NEXT_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }} \
-t registry.infra.mintel.me/mintel/klz-cables.com:latest .
--platform linux/arm64 \
--build-arg NEXT_PUBLIC_BASE_URL="$NEXT_PUBLIC_BASE_URL" \
--build-arg NEXT_PUBLIC_UMAMI_WEBSITE_ID="$NEXT_PUBLIC_UMAMI_WEBSITE_ID" \
--build-arg NEXT_PUBLIC_UMAMI_SCRIPT_URL="$NEXT_PUBLIC_UMAMI_SCRIPT_URL" \
--build-arg DIRECTUS_URL="$DIRECTUS_URL" \
-t registry.infra.mintel.me/mintel/klz-cables.com:$IMAGE_TAG \
--push .
- name: Push image
run: DOCKER_API_VERSION=1.44 docker push registry.infra.mintel.me/mintel/klz-cables.com:latest
# ──────────────────────────────────────────────────────────────────────────────
# JOB 4: Deploy via SSH
# ──────────────────────────────────────────────────────────────────────────────
deploy:
name: 🚀 Deploy
needs: [prepare, build]
runs-on: docker
env:
TARGET: ${{ needs.prepare.outputs.target }}
IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }}
ENV_FILE: ${{ needs.prepare.outputs.env_file }}
TRAEFIK_HOST: ${{ needs.prepare.outputs.traefik_host }}
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_BASE_URL || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_BASE_URL || secrets.TESTING_NEXT_PUBLIC_BASE_URL || secrets.NEXT_PUBLIC_BASE_URL) }}
NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.TESTING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID) }}
NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.TESTING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL) }}
SENTRY_DSN: ${{ needs.prepare.outputs.target == 'production' && secrets.SENTRY_DSN || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_SENTRY_DSN || secrets.TESTING_SENTRY_DSN || secrets.SENTRY_DSN) }}
MAIL_HOST: ${{ secrets.MAIL_HOST }}
MAIL_PORT: ${{ secrets.MAIL_PORT }}
MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }}
MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }}
MAIL_FROM: ${{ secrets.MAIL_FROM }}
MAIL_RECIPIENTS: ${{ secrets.MAIL_RECIPIENTS }}
DIRECTUS_URL: ${{ needs.prepare.outputs.target == 'production' && 'https://cms.klz-cables.com' || (needs.prepare.outputs.target == 'staging' && 'https://cms-staging.klz-cables.com' || 'https://cms-testing.klz-cables.com') }}
DIRECTUS_KEY: ${{ secrets.DIRECTUS_KEY }}
DIRECTUS_SECRET: ${{ secrets.DIRECTUS_SECRET }}
DIRECTUS_ADMIN_EMAIL: ${{ secrets.DIRECTUS_ADMIN_EMAIL }}
DIRECTUS_ADMIN_PASSWORD: ${{ secrets.DIRECTUS_ADMIN_PASSWORD }}
DIRECTUS_DB_NAME: ${{ secrets.DIRECTUS_DB_NAME || 'directus' }}
DIRECTUS_DB_USER: ${{ secrets.DIRECTUS_DB_USER || 'directus' }}
DIRECTUS_DB_PASSWORD: ${{ secrets.DIRECTUS_DB_PASSWORD }}
DIRECTUS_API_TOKEN: ${{ secrets.DIRECTUS_API_TOKEN }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup SSH
- name: 🚀 Deploy to ${{ env.TARGET }}
run: |
echo "Deploying $TARGET → $IMAGE_TAG"
mkdir -p ~/.ssh
printf "%s\n" "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519
echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts
ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null
- name: Prepare and Deploy
cat > /tmp/klz-cables.env << EOF
# Generated by CI - $TARGET - $(date -u)
NODE_ENV=production
NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
NEXT_PUBLIC_UMAMI_SCRIPT_URL=$NEXT_PUBLIC_UMAMI_SCRIPT_URL
SENTRY_DSN=$SENTRY_DSN
MAIL_HOST=$MAIL_HOST
MAIL_PORT=$MAIL_PORT
MAIL_USERNAME=$MAIL_USERNAME
MAIL_PASSWORD=$MAIL_PASSWORD
MAIL_FROM=$MAIL_FROM
MAIL_RECIPIENTS=$MAIL_RECIPIENTS
# Directus
DIRECTUS_URL=$DIRECTUS_URL
DIRECTUS_KEY=$DIRECTUS_KEY
DIRECTUS_SECRET=$DIRECTUS_SECRET
DIRECTUS_ADMIN_EMAIL=$DIRECTUS_ADMIN_EMAIL
DIRECTUS_ADMIN_PASSWORD=$DIRECTUS_ADMIN_PASSWORD
DIRECTUS_DB_NAME=$DIRECTUS_DB_NAME
DIRECTUS_DB_USER=$DIRECTUS_DB_USER
DIRECTUS_DB_PASSWORD=$DIRECTUS_DB_PASSWORD
DIRECTUS_API_TOKEN=$DIRECTUS_API_TOKEN
IMAGE_TAG=$IMAGE_TAG
TRAEFIK_HOST=$TRAEFIK_HOST
ENV_FILE=$ENV_FILE
EOF
scp -o StrictHostKeyChecking=accept-new /tmp/klz-cables.env root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/$ENV_FILE
scp -o StrictHostKeyChecking=accept-new docker-compose.yml root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/docker-compose.yml
ssh -o StrictHostKeyChecking=accept-new root@alpha.mintel.me IMAGE_TAG="$IMAGE_TAG" ENV_FILE="$ENV_FILE" TRAEFIK_HOST="$TRAEFIK_HOST" bash << 'EOF'
set -e
cd /home/deploy/sites/klz-cables.com
chmod 600 "$ENV_FILE"
chown deploy:deploy "$ENV_FILE"
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
echo "→ Pulling image: $IMAGE_TAG"
docker compose --env-file "$ENV_FILE" pull
echo "→ Starting containers..."
docker compose --env-file "$ENV_FILE" up -d
docker system prune -f --filter "until=168h"
echo "→ Waiting 15s for warmup..."
sleep 15
echo "→ Container status:"
docker compose --env-file "$ENV_FILE" ps
if ! docker compose --env-file "$ENV_FILE" ps | grep -q "Up"; then
echo "❌ Fehler: Container nicht Up!"
docker compose --env-file "$ENV_FILE" logs --tail=150
exit 1
fi
echo "✅ Deployment erfolgreich!"
EOF
rm -f /tmp/klz-cables.env
- name: 🎨 Branding Setup
if: success()
env:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASS: ${{ secrets.REGISTRY_PASS }}
NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}
NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
REDIS_URL: ${{ secrets.REDIS_URL }}
REDIS_KEY_PREFIX: ${{ secrets.REDIS_KEY_PREFIX }}
DIRECTUS_URL: ${{ env.DIRECTUS_URL }}
run: |
echo "Preparing deployment files..."
echo "🎨 Applying KLZ Branding to $TARGET..."
# Load the locally generated env but use the server URL
NEXT_PUBLIC_BASE_URL="${{ env.NEXT_PUBLIC_BASE_URL }}" \
DIRECTUS_ADMIN_EMAIL="${{ secrets.DIRECTUS_ADMIN_EMAIL }}" \
DIRECTUS_ADMIN_PASSWORD="${{ secrets.DIRECTUS_ADMIN_PASSWORD }}" \
npx tsx scripts/setup-directus-branding.ts
COMPOSE_FILE=""
if [ -f "docker-compose.yml" ]; then
COMPOSE_FILE="docker-compose.yml"
elif [ -f "docker-compose.yaml" ]; then
COMPOSE_FILE="docker-compose.yaml"
else
echo "ERROR: No docker-compose file found!"
exit 1
fi
# ──────────────────────────────────────────────────────────────────────────────
# JOB 5: Notifications
# ──────────────────────────────────────────────────────────────────────────────
notifications:
name: 🔔 Notifications
needs: [prepare, qa, build, deploy]
if: always()
runs-on: docker
steps:
- name: 📊 Deployment Summary
run: |
echo "┌──────────────────────────────┐"
echo "│ Deployment Summary │"
echo "├──────────────────────────────┤"
echo "│ Status: ${{ needs.deploy.result }} │"
echo "│ Umgebung: ${{ needs.prepare.outputs.target || 'skipped' }} │"
echo "│ Version: ${{ needs.prepare.outputs.image_tag }} │"
echo "│ Commit: ${{ needs.prepare.outputs.short_sha }} │"
echo "│ Message: ${{ needs.prepare.outputs.commit_msg }} │"
echo "└──────────────────────────────┘"
# Create a temporary directory for all files to sync
mkdir -p /tmp/klz-deploy
cp "$COMPOSE_FILE" /tmp/klz-deploy/docker-compose.yml
if [ -d "varnish" ]; then
cp -r varnish /tmp/klz-deploy/
fi
- name: 🔔 Gotify - Success
if: needs.deploy.result == 'success'
run: |
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
-F "title=${{ needs.prepare.outputs.gotify_title }}" \
-F "message=Erfolgreich deployt auf **${{ needs.prepare.outputs.target }}**\n\nVersion: **${{ needs.prepare.outputs.image_tag }}**\nCommit: ${{ needs.prepare.outputs.short_sha }} (${{ needs.prepare.outputs.commit_msg }})\nVon: ${{ github.actor }}\nRun: ${{ github.run_id }}" \
-F "priority=${{ needs.prepare.outputs.gotify_priority }}" || true
# Create environment file
cat > /tmp/klz-deploy/deploy.env << EOF
export REGISTRY_USER='${REGISTRY_USER}'
export REGISTRY_PASS='${REGISTRY_PASS}'
export NEXT_PUBLIC_UMAMI_WEBSITE_ID='${NEXT_PUBLIC_UMAMI_WEBSITE_ID}'
export NEXT_PUBLIC_UMAMI_SCRIPT_URL='${NEXT_PUBLIC_UMAMI_SCRIPT_URL}'
export SENTRY_DSN='${SENTRY_DSN}'
export REDIS_URL='${REDIS_URL}'
export REDIS_KEY_PREFIX='${REDIS_KEY_PREFIX}'
EOF
# Create deployment script
cat > /tmp/klz-deploy/deploy.sh << 'DEPLOY_EOF'
#!/bin/bash
set -e
# Load environment variables
source ./deploy.env
echo '=== Starting deployment ==='
echo '=== Creating .env ==='
cat > .env << EOF
NEXT_PUBLIC_UMAMI_WEBSITE_ID=${NEXT_PUBLIC_UMAMI_WEBSITE_ID}
NEXT_PUBLIC_UMAMI_SCRIPT_URL=${NEXT_PUBLIC_UMAMI_SCRIPT_URL}
SENTRY_DSN=${SENTRY_DSN}
REDIS_URL=${REDIS_URL}
REDIS_KEY_PREFIX=${REDIS_KEY_PREFIX}
EOF
echo '=== Logging into Docker registry ==='
echo "${REGISTRY_PASS}" | docker login registry.infra.mintel.me -u "${REGISTRY_USER}" --password-stdin
echo '=== Checking if infra network exists ==='
if ! docker network inspect infra >/dev/null 2>&1; then
echo 'Creating infra network...'
docker network create infra || true
fi
echo '=== Pulling latest image ==='
docker compose pull
echo '=== Restarting containers ==='
docker compose down
docker compose up -d
echo '=== Waiting for containers (30s) ==='
sleep 30
echo '=== Verifying health ==='
if curl -f -s http://localhost:80/health > /dev/null 2>&1; then
echo '✓ Application health check passed'
else
echo '✗ Application health check failed'
docker compose logs --tail=50 app
exit 1
fi
echo '=== Cleaning up ==='
docker image prune -f --filter 'until=24h'
echo '=== Deployment completed successfully ==='
DEPLOY_EOF
chmod +x /tmp/klz-deploy/deploy.sh
echo "Syncing and executing on server..."
# Use a single SSH connection to sync and execute
# We use tar to bundle everything and pipe it to SSH
# We use the same SSH options that worked in the previous Sync step
tar czf - -C /tmp/klz-deploy . | \
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o IPQoS=0x00 deploy@alpha.mintel.me \
"mkdir -p /home/deploy/sites/klz-cables.com/ && tar xzf - -C /home/deploy/sites/klz-cables.com/ && cd /home/deploy/sites/klz-cables.com/ && bash ./deploy.sh"
echo "Deployment process finished"
- name: 🔔 Gotify - Failure
if: needs.deploy.result == 'failure' || needs.build.result == 'failure' || needs.qa.result == 'failure'
run: |
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
-F "title=❌ Deployment FEHLGESCHLAGEN ${{ needs.prepare.outputs.target || 'unknown' }}" \
-F "message=**Fehler beim Deploy auf ${{ needs.prepare.outputs.target }}**\n\nVersion: ${{ needs.prepare.outputs.image_tag || '?' }}\nCommit: ${{ needs.prepare.outputs.short_sha || '?' }}\nVon: ${{ github.actor }}\nRun: ${{ github.run_id }}\n\nBitte Logs prüfen!" \
-F "priority=8" || true

7
.gitignore vendored
View File

@@ -1,2 +1,7 @@
node_modules
.next
.next
.DS_Store
# Directus
directus/uploads
!directus/extensions/

View File

@@ -3,7 +3,7 @@ FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
RUN apk add --no-cache libc6-compat curl
WORKDIR /app
# Install dependencies based on the preferred package manager
@@ -20,14 +20,22 @@ COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED 1
ENV NEXT_TELEMETRY_DISABLED=1
# Build-time environment variables for Next.js
# These are baked into the client bundle during build
ARG NEXT_PUBLIC_BASE_URL
ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
ARG NEXT_PUBLIC_UMAMI_SCRIPT_URL
ARG NEXT_PUBLIC_SENTRY_DSN
ARG DIRECTUS_URL
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
ENV NEXT_PUBLIC_UMAMI_SCRIPT_URL=$NEXT_PUBLIC_UMAMI_SCRIPT_URL
ENV NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN
ENV DIRECTUS_URL=$DIRECTUS_URL
# Validate environment variables during build
RUN npx tsx scripts/validate-env.ts
RUN npm run build
@@ -35,7 +43,10 @@ RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
# Install curl for health checks
RUN apk add --no-cache curl
ENV NODE_ENV=production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1
@@ -57,9 +68,9 @@ USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV PORT=3000
# set hostname to localhost
ENV HOSTNAME "0.0.0.0"
ENV HOSTNAME="0.0.0.0"
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output

272
ENV_CLEANUP_SUMMARY.md Normal file
View File

@@ -0,0 +1,272 @@
# Environment Variables Cleanup - Summary
## What Was Done
Cleaned up the fragile, overkill environment variable mess and replaced it with a simple, clean, robust **fully automated** system.
## Changes Made
### 1. Dockerfile ✅
**Before**: 4 build args including runtime-only variables (SENTRY_DSN)
**After**: 3 build args - only `NEXT_PUBLIC_*` variables that need to be baked into the client bundle
```dockerfile
# Only these build args now:
ARG NEXT_PUBLIC_BASE_URL
ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
ARG NEXT_PUBLIC_UMAMI_SCRIPT_URL
```
### 2. docker-compose.yml ✅
**Before**: 12+ individual environment variables listed
**After**: Single `env_file: .env` directive
```yaml
app:
image: registry.infra.mintel.me/mintel/klz-cables.com:latest
env_file:
- .env # All runtime vars loaded from here
```
### 3. .gitea/workflows/deploy.yml ✅
**Before**: Passing 12+ environment variables individually via SSH command (fragile!)
**After**: **Fully automated** - workflow creates `.env` file from Gitea secrets and uploads it
```yaml
# Before (FRAGILE):
ssh root@alpha.mintel.me \
"MAIL_FROM='${{ secrets.MAIL_FROM }}' \
MAIL_HOST='${{ secrets.MAIL_HOST }}' \
... (12+ variables) \
/home/deploy/deploy.sh"
# After (AUTOMATED):
# 1. Create .env from secrets
cat > /tmp/klz-cables.env << EOF
NODE_ENV=production
NEXT_PUBLIC_BASE_URL=${{ secrets.NEXT_PUBLIC_BASE_URL }}
# ... all other vars from secrets
EOF
# 2. Upload to server
scp /tmp/klz-cables.env root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/.env
# 3. Deploy
ssh root@alpha.mintel.me "cd /home/deploy/sites/klz-cables.com && docker-compose up -d"
```
### 4. New Files Created ✅
- **`.env.production`** - Template for reference (not used in automation)
- **`docs/DEPLOYMENT.md`** - Complete deployment guide
- **`docs/SERVER_SETUP.md`** - Server setup instructions
- **`docs/ENV_MIGRATION.md`** - Migration guide from old to new system
### 5. Updated Files ✅
- **`.env.example`** - Clear documentation of all variables with build-time vs runtime notes
## Architecture
### Build Time (CI/CD)
```
Gitea Workflow
Only passes NEXT_PUBLIC_* as --build-arg
Docker Build
Validates env vars
Bakes NEXT_PUBLIC_* into client bundle
Push to Registry
```
### Runtime (Production Server) - FULLY AUTOMATED
```
Gitea Secrets
Workflow creates .env file
SCP uploads to server
Secured (chmod 600, chown deploy:deploy)
docker-compose.yml (env_file: .env)
Loads .env into container
Application runs with full config
```
## Key Benefits
### 1. Simplicity
- **Before**: 15+ Gitea secrets, variables in 3+ places
- **After**: All secrets in Gitea, automatically deployed
### 2. Clarity
- **Before**: Confusing duplication, unclear which vars go where
- **After**: Clear separation - build args vs runtime env file
### 3. Robustness
- **Before**: Fragile SSH command with 12+ inline variables
- **After**: Robust automated file generation and upload
### 4. Security
- **Before**: Secrets potentially exposed in CI logs
- **After**: Secrets masked in logs, .env auto-secured on server
### 5. Maintainability
- **Before**: Update in 3 places (Dockerfile, docker-compose.yml, deploy.yml)
- **After**: Update Gitea secrets only - deployment is automatic
### 6. **Zero Manual Steps** 🎉
- **Before**: Manual .env file creation on server (error-prone, can be forgotten)
- **After**: **Fully automated** - .env file created and uploaded on every deployment
## What You Need to Do
### Required Gitea Secrets
Ensure these secrets are configured in your Gitea repository:
**Build-Time (NEXT_PUBLIC_*):**
- `NEXT_PUBLIC_BASE_URL` - Production URL (e.g., `https://klz-cables.com`)
- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` - Umami analytics ID
- `NEXT_PUBLIC_UMAMI_SCRIPT_URL` - Umami script URL
**Runtime:**
- `SENTRY_DSN` - Error tracking DSN
- `MAIL_HOST` - SMTP server
- `MAIL_PORT` - SMTP port (e.g., `587`)
- `MAIL_USERNAME` - SMTP username
- `MAIL_PASSWORD` - SMTP password
- `MAIL_FROM` - Sender email
- `MAIL_RECIPIENTS` - Recipient emails (comma-separated)
**Infrastructure:**
- `REGISTRY_USER` - Docker registry username
- `REGISTRY_PASS` - Docker registry password
- `ALPHA_SSH_KEY` - SSH private key for deployment server
**Notifications:**
- `GOTIFY_URL` - Gotify notification server URL
- `GOTIFY_TOKEN` - Gotify application token
### That's It!
**No manual steps required.** Just push to main branch and the workflow will:
1. ✅ Build Docker image with NEXT_PUBLIC_* build args
2. ✅ Create .env file from all secrets
3. ✅ Upload .env to server
4. ✅ Secure .env file (600 permissions, deploy:deploy ownership)
5. ✅ Pull latest image
6. ✅ Deploy with docker-compose
## Files Changed
```
Modified:
├── Dockerfile (removed redundant build args)
├── docker-compose.yml (use env_file instead of individual vars)
├── .gitea/workflows/deploy.yml (automated .env creation & upload)
├── .env.example (clear documentation)
├── lib/services/create-services.ts (removed redundant dotenv usage)
└── scripts/migrate-*.ts (removed redundant dotenv usage)
Created:
├── .env.production (reference template)
├── docs/DEPLOYMENT.md (deployment guide)
├── docs/SERVER_SETUP.md (server setup guide)
├── docs/ENV_MIGRATION.md (migration guide)
└── ENV_CLEANUP_SUMMARY.md (this file)
```
## Deployment Flow
```
┌─────────────────────────────────────────────────────────────┐
│ Developer pushes to main branch │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Gitea Workflow Triggered │
│ │
│ 1. Build Docker image (NEXT_PUBLIC_* build args) │
│ 2. Push to registry │
│ 3. Generate .env from secrets │
│ 4. Upload .env to server via SCP │
│ 5. SSH to server and deploy │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Production Server │
│ │
│ 1. .env file secured (600, deploy:deploy) │
│ 2. Docker login to registry │
│ 3. Pull latest image │
│ 4. docker-compose down │
│ 5. docker-compose up -d (loads .env) │
│ 6. Health checks pass │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ ✅ Deployment Complete - Gotify Notification Sent │
└─────────────────────────────────────────────────────────────┘
```
## Comparison: Before vs After
| Aspect | Before | After |
|--------|--------|-------|
| **Gitea Secrets** | 15+ secrets | Same secrets, better organized |
| **Build Args** | 4 vars (including runtime-only) | 3 vars (NEXT_PUBLIC_* only) |
| **Runtime Vars** | Passed via SSH command | Auto-generated .env file |
| **Manual Steps** | ❌ Manual .env creation | ✅ Fully automated |
| **Maintenance** | Update in 3 places | Update Gitea secrets only |
| **Security** | Secrets in CI logs | Secrets masked, .env secured |
| **Clarity** | Confusing duplication | Clear separation |
| **Robustness** | Fragile SSH command | Robust automation |
| **Error-Prone** | ❌ Can forget .env | ✅ Impossible to forget |
## Documentation
- **[DEPLOYMENT.md](docs/DEPLOYMENT.md)** - Complete deployment guide
- **[SERVER_SETUP.md](docs/SERVER_SETUP.md)** - Server setup instructions (mostly automated now)
- **[ENV_MIGRATION.md](docs/ENV_MIGRATION.md)** - Migration from old to new system
- **[.env.example](.env.example)** - Environment variables reference
- **[.env.production](.env.production)** - Production template (for reference)
## Troubleshooting
### Deployment Fails
1. **Check Gitea secrets** - Ensure all required secrets are set
2. **Check workflow logs** - Look for specific error messages
3. **SSH to server** - Verify .env file exists and has correct permissions
4. **Check container logs** - `docker-compose logs -f app`
### .env File Issues
The workflow automatically:
- Creates .env from secrets
- Uploads to server
- Sets 600 permissions
- Sets deploy:deploy ownership
If there are issues, check the workflow logs for the "📝 Preparing environment configuration" step.
### Missing Environment Variables
If a variable is missing:
1. Add it to Gitea secrets
2. Update `.gitea/workflows/deploy.yml` to include it in the .env generation
3. Push to trigger new deployment
---
**Result**: Environment variable management is now simple, clean, robust, and **fully automated**! 🎉
No more manual .env file creation. No more forgotten configuration. No more fragile SSH commands. Just push and deploy!

115
README.md
View File

@@ -29,6 +29,35 @@ npm run export
# Or run development server
npm run dev
### 🏗️ CMS (Strapi)
The CMS runs in Docker. Use the following npm scripts for local development:
```bash
# Start Strapi and its database
npm run cms:dev
# View logs
npm run cms:logs
# Stop the CMS
npm run cms:stop
```
Once running, you can access the Strapi admin panel at `http://localhost:1337/admin`.
### 🔄 Data & Migration
To sync data or migrate existing content:
```bash
# Export local data
npm run cms:export -- my-data.tar.gz
# Import data
npm run cms:import -- my-data.tar.gz
# Migrate existing MDX data to Strapi
npm run cms:migrate
```
### Environment Variables
@@ -47,11 +76,6 @@ NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
SENTRY_DSN=https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID
NEXT_PUBLIC_SENTRY_DSN=https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID
# Redis (optional cache)
# Platform provides a shared redis container reachable as `redis`.
# Pick a dedicated DB index per app, e.g. redis://redis:6379/2
REDIS_URL=redis://redis:6379/2
REDIS_KEY_PREFIX=klz:
```
## 📊 Project Overview
@@ -78,7 +102,8 @@ REDIS_KEY_PREFIX=klz:
- **Framework**: Next.js 14 (App Router)
- **Language**: TypeScript
- **Styling**: SCSS
- **Data**: Static JSON (WordPress export)
- **CMS**: Strapi (Source of Truth)
- **Data**: Static JSON (WordPress export) & Strapi API
- **Email**: Resend
- **Analytics**: Vercel (consent-based)
- **CAPTCHA**: Cloudflare Turnstile
@@ -108,7 +133,7 @@ app/
├── api/
│ └── contact/route.ts # Contact API
├── sitemap.ts # Sitemap generator
── robots.ts # Robots.txt generator
── robots.ts # Robots.txt generator
lib/
├── data.ts # Data access
@@ -119,7 +144,7 @@ components/
├── LocaleSwitcher.tsx # Language switcher
├── ContactForm.tsx # Contact form
├── CookieConsent.tsx # GDPR banner
── SEO.tsx # SEO utilities
── SEO.tsx # SEO utilities
data/
├── raw/ # WordPress export
@@ -225,31 +250,69 @@ GET /robots.txt
## 🚀 Deployment
### Vercel (Recommended)
```bash
# Install Vercel CLI
npm i -g vercel
### Automatic Deployment (Current Setup)
# Deploy
vercel --prod
The project uses **Gitea Actions** for CI/CD. Every push to `main` or `staging` triggers:
1. **Build**: Docker image built for `linux/arm64` with branch-specific build args
2. **Push**: Image pushed to `registry.infra.mintel.me` with commit SHA tag
3. **Deploy**: SSH to production server, pull and restart containers using branch-specific `.env` files
**Workflow**: `.gitea/workflows/deploy.yml`
**Branch Deployments**:
- `main` branch: Deploys to production using `.env.prod`
- `staging` branch: Deploys to staging using `.env.staging`
**Environment Overrides**:
The CI/CD workflow supports `STAGING_`-prefixed secrets (e.g., `STAGING_NEXT_PUBLIC_BASE_URL`) to override default secrets when deploying the `staging` branch.
**Required Secrets** (configure in Gitea repository settings):
- `REGISTRY_USER` - Docker registry username
- `REGISTRY_PASS` - Docker registry password
- `ALPHA_SSH_KEY` - SSH private key for deployment
- `NEXT_PUBLIC_BASE_URL` - Application base URL (e.g., `https://klz-cables.com`)
- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` - Analytics ID
- `NEXT_PUBLIC_UMAMI_SCRIPT_URL` - Analytics script URL
- `SENTRY_DSN` - Error tracking DSN
- `MAIL_HOST`, `MAIL_PORT`, `MAIL_USERNAME`, `MAIL_PASSWORD`, `MAIL_FROM`, `MAIL_RECIPIENTS` - Email configuration
### Manual Deployment
```bash
# SSH into production server
ssh deploy@alpha.mintel.me
# Navigate to project
cd /home/deploy/sites/klz-cables.com
# Pull latest image and restart
docker compose pull
docker compose up -d --force-recreate --remove-orphans
docker image prune -f
```
### Static Export
Or use the convenience script:
```bash
# Build and export
npm run build
npm run export
# Deploy to any static host
# Upload /out directory
bash scripts/deploy-webhook.sh
```
### Netlify
```bash
# Connect repository
# Set build command: npm run build
# Set publish directory: out
### Architecture
```
Client → Traefik (TLS) → Next.js App
```
**Domains**:
- `klz-cables.com` - Production
- `www.klz-cables.com` - Production (www)
- `staging.klz-cables.com` - Staging
**Services**:
- `app`: Next.js application (port 3000)
- `traefik`: Reverse proxy (external)
For detailed deployment documentation, see [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md).
## 📈 Performance

View File

@@ -0,0 +1,31 @@
import { ImageResponse } from 'next/og';
import { getPageBySlug } from '@/lib/pages';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs';
export default async function Image({ params: { locale, slug } }: { params: { locale: string, slug: string } }) {
const pageData = await getPageBySlug(slug, locale);
if (!pageData) {
return new Response('Page not found', { status: 404 });
}
const fonts = await getOgFonts();
return new ImageResponse(
(
<OGImageTemplate
title={pageData.frontmatter.title}
description={pageData.frontmatter.excerpt}
label="Information"
/>
),
{
...OG_IMAGE_SIZE,
fonts,
}
);
}

View File

@@ -1,8 +1,11 @@
import { notFound } from 'next/navigation';
import { MDXRemote } from 'next-mdx-remote/rsc';
import { Section, Container, Heading, Badge } from '@/components/ui';
import { Container, Badge, Heading } from '@/components/ui';
import { getTranslations } from 'next-intl/server';
import { Metadata } from 'next';
import { getPageBySlug, getAllPages } from '@/lib/pages';
import { mdxComponents } from '@/components/blog/MDXComponents';
import { getOGImageMetadata } from '@/lib/metadata';
interface PageProps {
params: {
@@ -11,8 +14,21 @@ interface PageProps {
};
}
export async function generateStaticParams() {
const locales = ['en', 'de'];
const params = [];
for (const locale of locales) {
const pages = await getAllPages(locale);
for (const page of pages) {
params.push({ locale, slug: page.slug });
}
}
return params;
}
export async function generateMetadata({ params: { locale, slug } }: PageProps): Promise<Metadata> {
const { getPageBySlug } = await import('@/lib/pages');
const pageData = await getPageBySlug(slug, locale);
if (!pageData) return {};
@@ -32,6 +48,7 @@ export async function generateMetadata({ params: { locale, slug } }: PageProps):
title: `${pageData.frontmatter.title} | KLZ Cables`,
description: pageData.frontmatter.excerpt || '',
url: `https://klz-cables.com/${locale}/${slug}`,
images: getOGImageMetadata(slug, pageData.frontmatter.title, locale),
},
twitter: {
card: 'summary_large_image',
@@ -42,7 +59,6 @@ export async function generateMetadata({ params: { locale, slug } }: PageProps):
}
export default async function StandardPage({ params: { locale, slug } }: PageProps) {
const { getPageBySlug } = await import('@/lib/pages');
const pageData = await getPageBySlug(slug, locale);
const t = await getTranslations('StandardPage');
@@ -51,7 +67,7 @@ export default async function StandardPage({ params: { locale, slug } }: PagePro
}
return (
<div className="flex flex-col min-h-screen bg-neutral-light">
<div className="flex flex-col min-h-screen bg-white">
{/* Hero Section */}
<section className="bg-primary-dark text-white py-20 md:py-32 relative overflow-hidden">
<div className="absolute inset-0 opacity-20">
@@ -60,67 +76,44 @@ export default async function StandardPage({ params: { locale, slug } }: PagePro
<Container className="relative z-10">
<div className="max-w-4xl animate-slide-up">
<Badge variant="accent" className="mb-4 md:mb-6">{t('badge')}</Badge>
<Heading level={1} className="text-3xl md:text-5xl lg:text-6xl text-white mb-0">
<span className="text-white">{pageData.frontmatter.title}</span>
<Heading level={1} className="text-white mb-0">
{pageData.frontmatter.title}
</Heading>
</div>
</Container>
</section>
<Section className="bg-white -mt-8 md:-mt-12 relative z-20 rounded-t-[32px] md:rounded-t-[60px] shadow-2xl py-12 md:py-28">
<Container>
<div className="sticky-narrative-container">
{/* Sticky Narrative Sidebar - Mobile Optimized */}
<div className="sticky-narrative-sidebar mb-8 lg:mb-0">
<div className="lg:sticky lg:top-32 space-y-4 md:space-y-8">
{/* Mobile-only chip/stepper feel */}
<div className="flex lg:hidden overflow-x-auto pb-4 gap-3 no-scrollbar -mx-4 px-4">
<Badge variant="primary" className="whitespace-nowrap px-4 py-2 rounded-full shadow-sm">{t('overview')}</Badge>
<Badge variant="neutral" className="whitespace-nowrap px-4 py-2 rounded-full shadow-sm opacity-60">{t('details')}</Badge>
<Badge variant="neutral" className="whitespace-nowrap px-4 py-2 rounded-full shadow-sm opacity-60">{t('support')}</Badge>
</div>
<div className="p-6 md:p-8 bg-neutral-light rounded-2xl md:rounded-3xl border border-neutral-medium shadow-sm">
<h3 className="text-lg md:text-xl font-bold text-primary mb-3 md:mb-4 flex items-center gap-2">
<span className="w-1.5 h-6 bg-accent rounded-full" />
{t('quickNavigation')}
</h3>
<nav className="space-y-3 md:space-y-4">
<p className="text-sm md:text-base text-text-secondary leading-relaxed">
{t('exploreDetails', { title: pageData.frontmatter.title })}
</p>
</nav>
</div>
<div className="p-6 md:p-8 bg-primary-dark rounded-2xl md:rounded-3xl text-white shadow-xl relative overflow-hidden group">
<div className="absolute top-0 right-0 w-24 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" />
<h3 className="text-lg md:text-xl font-bold mb-3 md:mb-4 relative z-10">{t('needHelp')}</h3>
<p className="text-sm md:text-base text-white/70 mb-4 md:mb-6 relative z-10">{t('supportTeamAvailable')}</p>
<a href={`/${locale}/contact`} className="inline-flex items-center text-accent font-bold hover:underline touch-target relative z-10 group/link">
{t('contactUs')}
<span className="ml-2 transition-transform group-hover/link:translate-x-1">&rarr;</span>
</a>
</div>
</div>
{/* Main Content Area */}
<div className="container mx-auto px-4 py-16 md:py-24">
<div className="max-w-4xl mx-auto">
{/* Excerpt/Lead paragraph if available */}
{pageData.frontmatter.excerpt && (
<div className="mb-16 animate-slight-fade-in-from-bottom">
<p className="text-xl md:text-2xl text-text-primary leading-relaxed font-medium border-l-4 border-primary pl-8 py-2 italic">
{pageData.frontmatter.excerpt}
</p>
</div>
)}
{/* Main Content */}
<div className="sticky-narrative-content">
<article className="prose prose-sm md:prose-lg lg:prose-xl prose-primary max-w-none
prose-headings:text-primary prose-headings:font-bold prose-headings:tracking-tight
prose-p:text-text-secondary prose-p:leading-relaxed
prose-strong:text-primary prose-strong:font-extrabold
prose-a:text-primary prose-a:font-bold prose-a:no-underline hover:prose-a:underline
prose-img:rounded-2xl md:prose-img:rounded-3xl prose-img:shadow-2xl
prose-ul:list-disc prose-ul:pl-5 md:prose-ul:pl-6
prose-li:text-text-secondary
">
<MDXRemote source={pageData.content} />
</article>
{/* Main content with shared blog components */}
<div className="prose prose-lg md:prose-xl max-w-none prose-headings:font-bold prose-headings:text-text-primary prose-p:text-text-secondary prose-p:leading-relaxed prose-a:text-primary prose-a:no-underline hover:prose-a:underline prose-img:rounded-2xl prose-img:shadow-2xl prose-blockquote:border-primary prose-blockquote:bg-primary/5 prose-blockquote:rounded-r-2xl prose-strong:text-primary animate-slight-fade-in-from-bottom">
<MDXRemote source={pageData.content} components={mdxComponents} />
</div>
{/* Support Section */}
<div className="mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group animate-slight-fade-in-from-bottom">
<div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" />
<div className="relative z-10 max-w-2xl">
<h3 className="text-2xl md:text-3xl font-bold mb-4">{t('needHelp')}</h3>
<p className="text-lg text-white/70 mb-8">{t('supportTeamAvailable')}</p>
<a href={`/${locale}/contact`} className="inline-flex items-center px-8 py-4 bg-accent text-primary-dark font-bold rounded-full hover:bg-white transition-all duration-300 group/link">
{t('contactUs')}
<span className="ml-2 transition-transform group-hover/link:translate-x-1">&rarr;</span>
</a>
</div>
</div>
</Container>
</Section>
</div>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,74 @@
import { ImageResponse } from 'next/og';
import { getProductBySlug } from '@/lib/mdx';
import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { NextRequest } from 'next/server';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs';
export async function GET(
request: NextRequest,
{ params }: { params: { locale: string } }
) {
const { searchParams, origin } = new URL(request.url);
const slug = searchParams.get('slug');
const locale = params.locale || 'en';
if (!slug) {
return new Response('Missing slug', { status: 400 });
}
const fonts = await getOgFonts();
const t = await getTranslations({ locale, namespace: 'Products' });
// Check if it's a category page
const categories = ['low-voltage-cables', 'medium-voltage-cables', 'high-voltage-cables', 'solar-cables'];
if (categories.includes(slug)) {
const categoryKey = slug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
const categoryTitle = t.has(`categories.${categoryKey}.title`) ? t(`categories.${categoryKey}.title`) : slug;
const categoryDesc = t.has(`categories.${categoryKey}.description`) ? t(`categories.${categoryKey}.description`) : '';
return new ImageResponse(
(
<OGImageTemplate
title={categoryTitle}
description={categoryDesc}
label="Product Category"
/>
),
{
...OG_IMAGE_SIZE,
fonts,
}
);
}
const product = await getProductBySlug(slug, locale);
if (!product) {
return new Response('Product not found', { status: 404 });
}
const featuredImage = product.frontmatter.images?.[0]
? (product.frontmatter.images[0].startsWith('http')
? product.frontmatter.images[0]
: `${origin}${product.frontmatter.images[0]}`)
: undefined;
return new ImageResponse(
(
<OGImageTemplate
title={product.frontmatter.title}
description={product.frontmatter.description}
label={product.frontmatter.categories?.[0] || 'Product'}
image={featuredImage}
/>
),
{
...OG_IMAGE_SIZE,
fonts,
}
);
}

View File

@@ -1,5 +1,7 @@
import { ImageResponse } from 'next/og';
import { getPostBySlug } from '@/lib/blog';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs';
@@ -7,107 +9,34 @@ export default async function Image({ params: { locale, slug } }: { params: { lo
const post = await getPostBySlug(slug, locale);
if (!post) {
return new ImageResponse(
<div style={{ display: 'flex', width: '100%', height: '100%', backgroundColor: '#001a4d' }} />
);
return new Response('Post not found', { status: 404 });
}
const fonts = await getOgFonts();
// We don't have request.url here, but we can assume the domain from SITE_URL or config
// For local images during dev, relative paths in <img> might not work in Satori
// but if we are in nodejs runtime, we could potentially read from disk.
// For now, let's just make sure it's absolute.
const featuredImage = post.frontmatter.featuredImage
? (post.frontmatter.featuredImage.startsWith('http')
? post.frontmatter.featuredImage
: `https://klz-cables.com${post.frontmatter.featuredImage}`)
: undefined;
return new ImageResponse(
(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'flex-end',
backgroundColor: '#001a4d',
padding: '80px',
position: 'relative',
}}
>
{/* Background Image Overlay if available */}
{post.frontmatter.featuredImage && (
<img
src={post.frontmatter.featuredImage.startsWith('http') ? post.frontmatter.featuredImage : `https://klz-cables.com${post.frontmatter.featuredImage}`}
alt=""
style={{
position: 'absolute',
inset: 0,
width: '100%',
height: '100%',
objectFit: 'cover',
opacity: 0.4,
}}
/>
)}
{/* Gradient Overlay */}
<div
style={{
position: 'absolute',
inset: 0,
background: 'linear-gradient(to top, rgba(0,26,77,1) 0%, rgba(0,26,77,0.4) 100%)',
}}
/>
<div style={{ display: 'flex', flexDirection: 'column', position: 'relative', zIndex: 10 }}>
{post.frontmatter.category && (
<div
style={{
fontSize: '20px',
fontWeight: 'bold',
color: '#00ff99',
textTransform: 'uppercase',
letterSpacing: '0.1em',
marginBottom: '16px',
backgroundColor: 'rgba(0,255,153,0.1)',
padding: '4px 12px',
borderRadius: '4px',
}}
>
{post.frontmatter.category}
</div>
)}
<div
style={{
fontSize: '64px',
fontWeight: '900',
color: 'white',
lineHeight: '1.1',
maxWidth: '900px',
marginBottom: '24px',
}}
>
{post.frontmatter.title}
</div>
<div style={{ display: 'flex', alignItems: 'center' }}>
<div
style={{
fontSize: '24px',
color: 'rgba(255,255,255,0.6)',
fontWeight: '500',
}}
>
KLZ Cables Blog
</div>
<div style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: '#00ff99', margin: '0 16px' }} />
<div
style={{
fontSize: '24px',
color: 'rgba(255,255,255,0.6)',
}}
>
{new Date(post.frontmatter.date).toLocaleDateString(locale, { year: 'numeric', month: 'long', day: 'numeric' })}
</div>
</div>
</div>
</div>
<OGImageTemplate
title={post.frontmatter.title}
description={post.frontmatter.excerpt}
label={post.frontmatter.category || 'Blog'}
image={featuredImage}
/>
),
{
width: 1200,
height: 630,
...OG_IMAGE_SIZE,
fonts,
}
);
}

View File

@@ -5,6 +5,13 @@ import { getBreadcrumbSchema, SITE_URL, LOGO_URL } from '@/lib/schema';
import { MDXRemote } from 'next-mdx-remote/rsc';
import { getPostBySlug, getAdjacentPosts, getReadingTime, getHeadings } from '@/lib/blog';
import { Metadata } from 'next';
import Link from 'next/link';
import PostNavigation from '@/components/blog/PostNavigation';
import PowerCTA from '@/components/blog/PowerCTA';
import TableOfContents from '@/components/blog/TableOfContents';
import { mdxComponents } from '@/components/blog/MDXComponents';
import { Heading } from '@/components/ui';
import { getOGImageMetadata } from '@/lib/metadata';
interface BlogPostProps {
params: {
@@ -37,6 +44,7 @@ export async function generateMetadata({ params: { locale, slug } }: BlogPostPro
publishedTime: post.frontmatter.date,
authors: ['KLZ Cables'],
url: `https://klz-cables.com/${locale}/blog/${slug}`,
images: getOGImageMetadata(`blog/${slug}`, post.frontmatter.title, locale),
},
twitter: {
card: 'summary_large_image',
@@ -46,151 +54,6 @@ export async function generateMetadata({ params: { locale, slug } }: BlogPostPro
};
}
import Link from 'next/link';
import VisualLinkPreview from '@/components/blog/VisualLinkPreview';
import { Callout } from '@/components/ui';
import HighlightBox from '@/components/blog/HighlightBox';
import Stats from '@/components/blog/Stats';
import AnimatedImage from '@/components/blog/AnimatedImage';
import ChatBubble from '@/components/blog/ChatBubble';
import SplitHeading from '@/components/blog/SplitHeading';
import PostNavigation from '@/components/blog/PostNavigation';
import PowerCTA from '@/components/blog/PowerCTA';
import TableOfContents from '@/components/blog/TableOfContents';
import StickyNarrative from '@/components/blog/StickyNarrative';
import TechnicalGrid from '@/components/blog/TechnicalGrid';
import ComparisonGrid from '@/components/blog/ComparisonGrid';
const components = {
VisualLinkPreview,
Callout,
HighlightBox,
Stats,
AnimatedImage,
ChatBubble,
PowerCTA,
SplitHeading,
StickyNarrative,
TechnicalGrid,
ComparisonGrid,
h1: () => null,
a: ({ href, children, ...props }: any) => {
if (href?.startsWith('/')) {
return (
<Link href={href} {...props} className="text-primary font-medium hover:underline decoration-2 underline-offset-2 transition-all">
{children}
</Link>
);
}
return (
<a
href={href}
{...props}
target="_blank"
rel="noopener noreferrer"
className="text-primary font-medium hover:underline decoration-2 underline-offset-2 transition-all inline-flex items-center gap-1"
>
{children}
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
);
},
img: (props: any) => (
<AnimatedImage src={props.src} alt={props.alt} />
),
h2: ({ children, ...props }: any) => (
<SplitHeading {...props} className="mt-16 mb-6 pb-3 border-b-2 border-primary/20">
{children}
</SplitHeading>
),
h3: ({ children, ...props }: any) => (
<h3 {...props} className="text-2xl font-bold text-text-primary mt-12 mb-4">
{children}
</h3>
),
p: ({ children, ...props }: any) => (
<p {...props} className="text-lg text-text-secondary leading-relaxed mb-6">
{children}
</p>
),
ul: ({ children, ...props }: any) => (
<ul {...props} className="my-8 space-y-3">
{children}
</ul>
),
ol: ({ children, ...props }: any) => (
<ol {...props} className="my-8 space-y-3 list-decimal list-inside">
{children}
</ol>
),
li: ({ children, ...props }: any) => (
<li {...props} className="text-lg text-text-secondary flex items-start gap-3">
<span className="text-primary mt-1.5 flex-shrink-0">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
</span>
<span className="flex-1">{children}</span>
</li>
),
blockquote: ({ children, ...props }: any) => (
<blockquote {...props} className="my-8 pl-6 border-l-4 border-primary bg-neutral-light/30 py-4 pr-6 rounded-r-lg">
<div className="text-lg text-text-primary italic">
{children}
</div>
</blockquote>
),
strong: ({ children, ...props }: any) => (
<strong {...props} className="font-bold text-primary">
{children}
</strong>
),
code: ({ children, ...props }: any) => (
<code {...props} className="px-2 py-1 bg-neutral-light text-primary rounded font-mono text-sm">
{children}
</code>
),
pre: ({ children, ...props }: any) => (
<pre {...props} className="my-8 p-6 bg-neutral-dark/5 rounded-xl overflow-x-auto">
{children}
</pre>
),
table: ({ children, ...props }: any) => (
<div className="my-8 overflow-x-auto rounded-lg border border-neutral-200 shadow-sm">
<table {...props} className="w-full text-left text-sm text-text-secondary">
{children}
</table>
</div>
),
thead: ({ children, ...props }: any) => (
<thead {...props} className="bg-neutral-50 text-text-primary font-semibold border-b border-neutral-200">
{children}
</thead>
),
tbody: ({ children, ...props }: any) => (
<tbody {...props} className="divide-y divide-neutral-200 bg-white">
{children}
</tbody>
),
tr: ({ children, ...props }: any) => (
<tr {...props} className="hover:bg-neutral-50/50 transition-colors">
{children}
</tr>
),
th: ({ children, ...props }: any) => (
<th {...props} className="px-6 py-4 whitespace-nowrap">
{children}
</th>
),
td: ({ children, ...props }: any) => (
<td {...props} className="px-6 py-4">
{children}
</td>
),
};
export default async function BlogPost({ params: { locale, slug } }: BlogPostProps) {
const post = await getPostBySlug(slug, locale);
const { prev, next } = await getAdjacentPosts(slug, locale);
@@ -224,9 +87,9 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
</span>
</div>
)}
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-8 leading-[1.1] drop-shadow-2xl animate-slight-fade-in-from-bottom [animation-delay:200ms]">
<Heading level={1} className="text-white mb-8 drop-shadow-2xl animate-slight-fade-in-from-bottom [animation-delay:200ms]">
{post.frontmatter.title}
</h1>
</Heading>
<div className="flex flex-wrap items-center gap-6 text-white/80 text-sm md:text-base font-medium animate-slight-fade-in-from-bottom [animation-delay:400ms]">
<time dateTime={post.frontmatter.date}>
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
@@ -252,9 +115,9 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
</span>
</div>
)}
<h1 className="text-4xl md:text-5xl font-bold text-text-primary mb-8 leading-tight">
<Heading level={1} className="mb-8">
{post.frontmatter.title}
</h1>
</Heading>
<div className="flex items-center gap-6 text-text-secondary font-medium">
<time dateTime={post.frontmatter.date}>
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
@@ -286,7 +149,7 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
{/* Main content with enhanced styling */}
<div className="prose prose-lg md:prose-xl max-w-none prose-headings:font-bold prose-headings:text-text-primary prose-p:text-text-secondary prose-p:leading-relaxed prose-a:text-primary prose-a:no-underline hover:prose-a:underline prose-img:rounded-2xl prose-img:shadow-2xl prose-blockquote:border-primary prose-blockquote:bg-primary/5 prose-blockquote:rounded-r-2xl prose-strong:text-primary animate-slight-fade-in-from-bottom [animation-delay:800ms]">
<MDXRemote source={post.content} components={components} />
<MDXRemote source={post.content} components={mdxComponents} />
</div>
{/* Power CTA */}
@@ -336,14 +199,14 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
'@type': 'Organization',
name: 'KLZ Cables',
url: 'https://klz-cables.com',
logo: 'https://klz-cables.com/logo.png'
logo: 'https://klz-cables.com/logo-blue.svg'
},
publisher: {
'@type': 'Organization',
name: 'KLZ Cables',
logo: {
'@type': 'ImageObject',
url: 'https://klz-cables.com/logo.png',
url: 'https://klz-cables.com/logo-blue.svg',
},
},
description: post.frontmatter.excerpt,
@@ -354,7 +217,7 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
articleSection: post.frontmatter.category,
wordCount: post.content.split(/\s+/).length,
timeRequired: `PT${getReadingTime(post.content)}M`
}}
} as any}
/>
<JsonLd
id={`breadcrumb-${slug}`}
@@ -375,7 +238,7 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
item: `https://klz-cables.com/${locale}/blog/${slug}`,
},
],
}}
} as any}
/>
</article>
);

View File

@@ -0,0 +1,25 @@
import { ImageResponse } from 'next/og';
import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs';
export default async function Image({ params: { locale } }: { params: { locale: string } }) {
const t = await getTranslations({ locale, namespace: 'Blog.meta' });
const fonts = await getOgFonts();
return new ImageResponse(
(
<OGImageTemplate
title={t('title')}
description={t('description')}
label="Blog"
/>
),
{
...OG_IMAGE_SIZE,
fonts,
}
);
}

View File

@@ -3,6 +3,7 @@ import { getAllPosts } from '@/lib/blog';
import { Section, Container, Heading, Card, Badge, Button } from '@/components/ui';
import Reveal from '@/components/Reveal';
import { getTranslations } from 'next-intl/server';
import { getOGImageMetadata } from '@/lib/metadata';
interface BlogIndexProps {
params: {
@@ -27,6 +28,7 @@ export async function generateMetadata({ params: { locale } }: BlogIndexProps) {
title: `${t('title')} | KLZ Cables`,
description: t('description'),
url: `https://klz-cables.com/${locale}/blog`,
images: getOGImageMetadata('blog', t('title'), locale),
},
twitter: {
card: 'summary_large_image',
@@ -51,38 +53,40 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
return (
<div className="bg-neutral-light min-h-screen">
{/* Hero Section - Immersive Magazine Feel */}
<section className="relative h-[50vh] md:h-[70vh] min-h-[400px] md:min-h-[600px] flex items-center overflow-hidden bg-primary-dark">
{featuredPost && featuredPost.frontmatter.featuredImage && (
<>
<img
src={featuredPost.frontmatter.featuredImage}
alt={featuredPost.frontmatter.title}
className="absolute inset-0 w-full h-full object-cover scale-105 animate-slow-zoom opacity-40 md:opacity-60"
/>
<div className="absolute inset-0 image-overlay-gradient" />
</>
)}
<Container className="relative z-10">
<div className="max-w-4xl animate-slide-up">
<Badge variant="saturated" className="mb-4 md:mb-6">{t('featuredPost')}</Badge>
{featuredPost && (
<>
<h1 className="text-3xl md:text-6xl font-extrabold text-white mb-4 md:mb-8 leading-[1.1] line-clamp-3 md:line-clamp-none">
{featuredPost.frontmatter.title}
</h1>
<p className="text-base md:text-xl text-white/80 mb-6 md:mb-10 line-clamp-2 md:line-clamp-2 max-w-2xl">
{featuredPost.frontmatter.excerpt}
</p>
<Button href={`/${locale}/blog/${featuredPost.slug}`} variant="accent" size="lg" className="group w-full md:w-auto md:h-16 md:px-10 md:text-xl">
{t('readFullArticle')}
<span className="ml-3 transition-transform group-hover:translate-x-2">&rarr;</span>
</Button>
</>
)}
</div>
</Container>
</section>
<Reveal>
<section className="relative h-[50vh] md:h-[70vh] min-h-[400px] md:min-h-[600px] flex items-center overflow-hidden bg-primary-dark">
{featuredPost && featuredPost.frontmatter.featuredImage && (
<>
<img
src={featuredPost.frontmatter.featuredImage}
alt={featuredPost.frontmatter.title}
className="absolute inset-0 w-full h-full object-cover scale-105 animate-slow-zoom opacity-40 md:opacity-60"
/>
<div className="absolute inset-0 image-overlay-gradient" />
</>
)}
<Container className="relative z-10">
<div className="max-w-4xl animate-slide-up">
<Badge variant="saturated" className="mb-4 md:mb-6">{t('featuredPost')}</Badge>
{featuredPost && (
<>
<Heading level={1} className="text-white mb-4 md:mb-8">
{featuredPost.frontmatter.title}
</Heading>
<p className="text-base md:text-xl text-white/80 mb-6 md:mb-10 line-clamp-2 md:line-clamp-2 max-w-2xl">
{featuredPost.frontmatter.excerpt}
</p>
<Button href={`/${locale}/blog/${featuredPost.slug}`} variant="accent" size="lg" className="group w-full md:w-auto md:h-16 md:px-10 md:text-xl">
{t('readFullArticle')}
<span className="ml-3 transition-transform group-hover:translate-x-2">&rarr;</span>
</Button>
</>
)}
</div>
</Container>
</section>
</Reveal>
<Section className="bg-neutral-light py-12 md:py-28">
<Container>

View File

@@ -0,0 +1,28 @@
import { ImageResponse } from 'next/og';
import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs';
export default async function Image({ params: { locale } }: { params: { locale: string } }) {
const t = await getTranslations({ locale, namespace: 'Contact' });
const fonts = await getOgFonts();
const title = t('meta.title') || t('title');
const description = t('meta.description') || t('subtitle');
return new ImageResponse(
(
<OGImageTemplate
title={title}
description={description}
label="Contact"
/>
),
{
...OG_IMAGE_SIZE,
fonts,
}
);
}

View File

@@ -1,10 +1,21 @@
import { useTranslations } from 'next-intl';
import { getTranslations } from 'next-intl/server';
import { Metadata } from 'next';
import JsonLd from '@/components/JsonLd';
import { getBreadcrumbSchema, SITE_URL, LOGO_URL } from '@/lib/schema';
import { Section, Container, Heading } from '@/components/ui';
import ContactForm from '@/components/ContactForm';
import JsonLd from '@/components/JsonLd';
import Reveal from '@/components/Reveal';
import { Container, Heading, Section } from '@/components/ui';
import { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import { SITE_URL } from '@/lib/schema';
import { getOGImageMetadata } from '@/lib/metadata';
import { Suspense } from 'react';
import dynamic from 'next/dynamic';
const LeafletMap = dynamic(() => import('@/components/LeafletMap'), {
ssr: false,
loading: () => (
<div className="h-full w-full bg-neutral-medium flex items-center justify-center">
<div className="animate-pulse text-primary font-medium">Loading Map...</div>
</div>
),
});
interface ContactPageProps {
params: {
@@ -14,34 +25,47 @@ interface ContactPageProps {
export async function generateMetadata({ params: { locale } }: ContactPageProps): Promise<Metadata> {
const t = await getTranslations({ locale, namespace: 'Contact' });
const title = t('meta.title') || t('title');
const description = t('meta.description') || t('subtitle');
return {
title: t('title'),
description: t('subtitle'),
title,
description,
alternates: {
canonical: `/${locale}/contact`,
canonical: `https://klz-cables.com/${locale}/contact`,
languages: {
'de': '/de/contact',
'en': '/en/contact',
'x-default': '/en/contact',
'de-DE': '/de/contact',
'en-US': '/en/contact',
},
},
openGraph: {
title: `${t('title')} | KLZ Cables`,
description: t('subtitle'),
title: `${title} | KLZ Cables`,
description,
url: `https://klz-cables.com/${locale}/contact`,
siteName: 'KLZ Cables',
images: getOGImageMetadata('contact', title, locale),
locale: `${locale.toUpperCase()}_DE`,
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: `${t('title')} | KLZ Cables`,
description: t('subtitle'),
title: `${title} | KLZ Cables`,
description,
images: [`${SITE_URL}/${locale}/contact/opengraph-image`],
},
robots: {
index: true,
follow: true,
},
};
}
export default function ContactPage() {
const t = useTranslations('Contact');
const locale = t('locale') || 'en'; // Fallback if needed, but usually passed via params
export async function generateStaticParams() {
return [{ locale: 'de' }, { locale: 'en' }];
}
export default async function ContactPage({ params }: ContactPageProps) {
const { locale } = params;
const t = await getTranslations({ locale, namespace: 'Contact' });
return (
<div className="flex flex-col min-h-screen bg-neutral-light">
<JsonLd
@@ -68,18 +92,17 @@ export default function ContactPage() {
image: 'https://klz-cables.com/logo.png',
'@id': 'https://klz-cables.com',
url: 'https://klz-cables.com',
telephone: '+49 881 92537298',
address: {
'@type': 'PostalAddress',
streetAddress: 'Trifthofstraße 57',
addressLocality: 'Weilheim i. OB',
postalCode: '82362',
streetAddress: 'Raiffeisenstraße 22',
addressLocality: 'Remshalden',
postalCode: '73630',
addressCountry: 'DE',
},
geo: {
'@type': 'GeoCoordinates',
latitude: 47.8407,
longitude: 11.1421,
latitude: 48.8144,
longitude: 9.4144,
},
openingHoursSpecification: [
{
@@ -101,21 +124,23 @@ export default function ContactPage() {
}}
/>
{/* Hero Section */}
<section className="bg-primary-dark text-white py-20 md:py-32 relative overflow-hidden">
<div className="absolute inset-0 opacity-20">
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-accent via-transparent to-transparent" />
</div>
<Container className="relative z-10">
<div className="max-w-4xl animate-slide-up">
<Heading level={1} subtitle={t('heroSubtitle')} className="text-white mb-4 md:mb-6">
<span className="text-white">{t('title')}</span>
</Heading>
<p className="text-lg md:text-xl text-white/70 leading-relaxed max-w-2xl">
{t('subtitle')}
</p>
<Reveal>
<section className="bg-primary-dark text-white py-20 md:py-32 relative overflow-hidden">
<div className="absolute inset-0 opacity-20">
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-accent via-transparent to-transparent" />
</div>
</Container>
</section>
<Container className="relative z-10">
<div className="max-w-4xl">
<Heading level={1} subtitle={t('heroSubtitle')} className="text-white mb-4 md:mb-6">
<span className="text-white">{t('title')}</span>
</Heading>
<p className="text-lg md:text-xl text-white/70 leading-relaxed max-w-2xl">
{t('subtitle')}
</p>
</div>
</Container>
</section>
</Reveal>
<Section className="bg-neutral-light -mt-8 md:-mt-20 relative z-20 py-12 md:py-28">
<Container>
@@ -142,17 +167,6 @@ export default function ContactPage() {
</div>
</div>
<div className="flex items-start gap-4 md:gap-6 group">
<div className="w-10 h-10 md:w-14 md:h-14 rounded-xl md:rounded-2xl bg-saturated/10 flex items-center justify-center text-saturated group-hover:bg-accent group-hover:text-primary-dark transition-all duration-300 shadow-sm flex-shrink-0">
<svg className="w-5 h-5 md:w-7 md:h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
</div>
<div>
<h4 className="text-base md:text-xl font-bold text-primary mb-1 md:mb-2">{t('info.phone')}</h4>
<a href="tel:+4988192537298" className="text-sm md:text-lg text-text-secondary hover:text-primary transition-colors font-medium touch-target">+49 881 92537298</a>
</div>
</div>
<div className="flex items-start gap-4 md:gap-6 group">
<div className="w-10 h-10 md:w-14 md:h-14 rounded-xl md:rounded-2xl bg-saturated/10 flex items-center justify-center text-saturated group-hover:bg-accent group-hover:text-primary-dark transition-all duration-300 shadow-sm flex-shrink-0">
@@ -162,7 +176,7 @@ export default function ContactPage() {
</div>
<div>
<h4 className="text-base md:text-xl font-bold text-primary mb-1 md:mb-2">{t('info.email')}</h4>
<a href="mailto:info@klz-vertriebs-gmbh.com" className="text-sm md:text-lg text-text-secondary hover:text-primary transition-colors font-medium touch-target">info@klz-vertriebs-gmbh.com</a>
<a href="mailto:info@klz-cables.com" className="text-sm md:text-lg text-text-secondary hover:text-primary transition-colors font-medium touch-target">info@klz-cables.com</a>
</div>
</div>
</div>
@@ -170,7 +184,7 @@ export default function ContactPage() {
<div className="p-6 md:p-10 bg-white rounded-2xl md:rounded-3xl border border-neutral-medium shadow-sm animate-fade-in">
<Heading level={4} className="mb-4 md:mb-6">{t('hours.title')}</Heading>
<ul className="space-y-2 md:space-y-4">
<ul className="space-y-2 md:space-y-4 list-none m-0 p-0">
<li className="flex justify-between items-center pb-2 md:pb-4 border-b border-neutral-medium text-sm md:text-base">
<span className="font-bold text-primary">{t('hours.weekdays')}</span>
<span className="text-text-secondary">{t('hours.weekdaysTime')}</span>
@@ -185,24 +199,25 @@ export default function ContactPage() {
{/* Contact Form */}
<div className="lg:col-span-7">
<ContactForm />
<Suspense fallback={<div className="animate-pulse bg-neutral-medium h-96 rounded-2xl md:rounded-3xl"></div>}>
<ContactForm />
</Suspense>
</div>
</div>
</Container>
</Section>
{/* Map Placeholder */}
{/* Map Section */}
<section className="h-[300px] md:h-[500px] bg-neutral-medium relative overflow-hidden grayscale hover:grayscale-0 transition-all duration-1000">
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center">
<div className="w-20 h-20 bg-primary rounded-full flex items-center justify-center text-white mb-4 mx-auto shadow-2xl animate-bounce">
<svg className="w-10 h-10" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
</svg>
</div>
<p className="font-bold text-primary text-xl">{t('map.comingSoon')}</p>
</div>
</div>
<Suspense fallback={<div className="h-full w-full bg-neutral-medium animate-pulse flex items-center justify-center">
<div className="text-primary font-medium">Loading Map...</div>
</div>}>
<LeafletMap
address={t('info.address')}
lat={48.8144}
lng={9.4144}
/>
</Suspense>
</section>
</div>
);

View File

@@ -1,8 +1,8 @@
'use client';
import * as Sentry from '@sentry/nextjs';
import { useEffect } from 'react';
import { useTranslations } from 'next-intl';
import { getAppServices } from '@/lib/services/create-services';
import { Container, Button, Heading } from '@/components/ui';
import Scribble from '@/components/Scribble';
@@ -16,7 +16,13 @@ export default function Error({
const t = useTranslations('Error');
useEffect(() => {
Sentry.captureException(error);
const services = getAppServices();
services.errors.captureException(error);
services.logger.error('Application error caught by boundary', {
message: error.message,
stack: error.stack,
digest: error.digest
});
}, [error]);
return (

View File

@@ -1,11 +1,16 @@
import Footer from '@/components/Footer';
import Header from '@/components/Header';
import JsonLd from '@/components/JsonLd';
import AnalyticsProvider from '@/components/analytics/AnalyticsProvider';
import { Viewport } from 'next';
import { Metadata, Viewport } from 'next';
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import '../../styles/globals.css';
import { SITE_URL } from '@/lib/schema';
export const metadata: Metadata = {
metadataBase: new URL(SITE_URL),
};
export const viewport: Viewport = {
width: 'device-width',
@@ -31,6 +36,7 @@ export default async function LocaleLayout({
<html lang={locale} className="scroll-smooth overflow-x-hidden">
<body className="flex flex-col min-h-screen font-sans selection:bg-accent selection:text-primary-dark antialiased overflow-x-hidden">
<NextIntlClientProvider messages={messages} locale={locale}>
<JsonLd />
<Header />
<main className="flex-grow animate-fade-in overflow-visible">
{children}

View File

@@ -1,92 +1,26 @@
import { ImageResponse } from 'next/og';
import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'edge';
export const runtime = 'nodejs';
export default async function Image({ params: { locale } }: { params: { locale: string } }) {
const t = await getTranslations({ locale, namespace: 'Index.meta' });
const fonts = await getOgFonts();
return new ImageResponse(
(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'center',
backgroundColor: '#001a4d', // Primary Blue from Styleguide
padding: '80px',
position: 'relative',
}}
>
{/* Background Pattern / Scribble placeholder */}
<div
style={{
position: 'absolute',
top: '-100px',
right: '-100px',
width: '600px',
height: '600px',
borderRadius: '50%',
background: 'radial-gradient(circle, rgba(0,255,153,0.1) 0%, transparent 70%)',
}}
/>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div
style={{
fontSize: '24px',
fontWeight: 'bold',
color: '#00ff99', // Accent Green
textTransform: 'uppercase',
letterSpacing: '0.2em',
marginBottom: '20px',
}}
>
KLZ Cables
</div>
<div
style={{
fontSize: '72px',
fontWeight: '900',
color: 'white',
lineHeight: '1.1',
maxWidth: '800px',
marginBottom: '30px',
}}
>
{t('title')}
</div>
<div
style={{
fontSize: '32px',
color: 'rgba(255,255,255,0.7)',
maxWidth: '700px',
}}
>
{t('description')}
</div>
</div>
{/* Bottom Accent Line */}
<div
style={{
position: 'absolute',
bottom: '80px',
left: '80px',
width: '120px',
height: '8px',
backgroundColor: '#00ff99',
borderRadius: '4px',
}}
/>
</div>
<OGImageTemplate
title={t('title')}
description={t('description')}
label="Reliable Energy Infrastructure"
/>
),
{
width: 1200,
height: 630,
...OG_IMAGE_SIZE,
fonts,
}
);
}

View File

@@ -1,6 +1,6 @@
import Hero from '@/components/home/Hero';
import JsonLd from '@/components/JsonLd';
import { getBreadcrumbSchema } from '@/lib/schema';
import { getBreadcrumbSchema, SITE_URL } from '@/lib/schema';
import ProductCategories from '@/components/home/ProductCategories';
import WhatWeDo from '@/components/home/WhatWeDo';
import RecentPosts from '@/components/home/RecentPosts';
@@ -11,6 +11,9 @@ import GallerySection from '@/components/home/GallerySection';
import VideoSection from '@/components/home/VideoSection';
import CTA from '@/components/home/CTA';
import Reveal from '@/components/Reveal';
import { getTranslations } from 'next-intl/server';
import { Metadata } from 'next';
import { getOGImageMetadata } from '@/lib/metadata';
export default function HomePage({ params: { locale } }: { params: { locale: string } }) {
return (
@@ -34,3 +37,46 @@ export default function HomePage({ params: { locale } }: { params: { locale: str
</div>
);
}
export async function generateMetadata({ params: { locale } }: { params: { locale: string } }): Promise<Metadata> {
// Use translations for meta where available (namespace: Index.meta)
// Fallback to a sensible default if translation keys are missing.
let t;
try {
t = await getTranslations({ locale, namespace: 'Index.meta' });
} catch (err) {
// If translations for Index.meta are not present, try generic Index namespace
try {
t = await getTranslations({ locale, namespace: 'Index' });
} catch (e) {
t = (key: string) => '';
}
}
const title = t('title') || 'KLZ Cables';
const description = t('description') || '';
return {
title,
description,
alternates: {
canonical: `/${locale}`,
languages: {
'de': '/de',
'en': '/en',
'x-default': '/en',
},
},
openGraph: {
title: `${title} | KLZ Cables`,
description,
url: `https://klz-cables.com/${locale}`,
images: getOGImageMetadata('', title, locale),
},
twitter: {
card: 'summary_large_image',
title: `${title} | KLZ Cables`,
description,
},
};
}

View File

@@ -5,12 +5,14 @@ import ProductSidebar from '@/components/ProductSidebar';
import ProductTabs from '@/components/ProductTabs';
import ProductTechnicalData from '@/components/ProductTechnicalData';
import RelatedProducts from '@/components/RelatedProducts';
import { Badge, Container, Section } from '@/components/ui';
import DatasheetDownload from '@/components/DatasheetDownload';
import { Badge, Container, Heading, Section } from '@/components/ui';
import { getDatasheetPath } from '@/lib/datasheets';
import { getAllProducts, getProductBySlug } from '@/lib/mdx';
import { mapFileSlugToTranslated } from '@/lib/slugs';
import { mapFileSlugToTranslated, mapSlugToFileSlug } from '@/lib/slugs';
import { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import { getProductOGImageMetadata } from '@/lib/metadata';
import { MDXRemote } from 'next-mdx-remote/rsc';
import Image from 'next/image';
import Link from 'next/link';
@@ -30,9 +32,10 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
// Check if it's a category page
const categories = ['low-voltage-cables', 'medium-voltage-cables', 'high-voltage-cables', 'solar-cables'];
if (categories.includes(productSlug)) {
const categoryKey = productSlug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
const categoryTitle = t.has(`categories.${categoryKey}.title`) ? t(`categories.${categoryKey}.title`) : productSlug;
const fileSlug = await mapSlugToFileSlug(productSlug, locale);
if (categories.includes(fileSlug)) {
const categoryKey = fileSlug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
const categoryTitle = t.has(`categories.${categoryKey}.title`) ? t(`categories.${categoryKey}.title`) : fileSlug;
const categoryDesc = t.has(`categories.${categoryKey}.description`) ? t(`categories.${categoryKey}.description`) : '';
return {
@@ -50,6 +53,7 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
title: `${categoryTitle} | KLZ Cables`,
description: categoryDesc,
url: `https://klz-cables.com/${locale}/products/${productSlug}`,
images: getProductOGImageMetadata(fileSlug, categoryTitle, locale),
},
twitter: {
card: 'summary_large_image',
@@ -78,6 +82,7 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
description: product.frontmatter.description,
type: 'website',
url: `https://klz-cables.com/${locale}/products/${slug.join('/')}`,
images: getProductOGImageMetadata(productSlug, product.frontmatter.title, locale),
},
twitter: {
card: 'summary_large_image',
@@ -130,15 +135,17 @@ export default async function ProductPage({ params }: ProductPageProps) {
// Check if it's a category page
const categories = ['low-voltage-cables', 'medium-voltage-cables', 'high-voltage-cables', 'solar-cables'];
if (categories.includes(productSlug)) {
const fileSlug = await mapSlugToFileSlug(productSlug, locale);
if (categories.includes(fileSlug)) {
const allProducts = await getAllProducts(locale);
const categoryKey = productSlug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
const categoryTitle = t.has(`categories.${categoryKey}.title`) ? t(`categories.${categoryKey}.title`) : productSlug;
const categoryKey = fileSlug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
const categoryTitle = t.has(`categories.${categoryKey}.title`) ? t(`categories.${categoryKey}.title`) : fileSlug;
// Filter products for this category
const filteredProducts = allProducts.filter(p =>
p.frontmatter.categories.some(cat =>
cat.toLowerCase().replace(/\s+/g, '-') === productSlug ||
cat.toLowerCase().replace(/\s+/g, '-') === fileSlug ||
cat === categoryTitle
)
);
@@ -161,9 +168,9 @@ export default async function ProductPage({ params }: ProductPageProps) {
<span className="mx-3 opacity-30">/</span>
<span className="text-white/90">{categoryTitle}</span>
</nav>
<h1 className="text-5xl md:text-7xl lg:text-8xl font-extrabold text-white mb-8 tracking-tight leading-[1.05]">
<Heading level={1} className="text-white mb-8">
{categoryTitle}
</h1>
</Heading>
<div className="h-1.5 w-24 bg-accent rounded-full" />
</div>
</Container>
@@ -245,8 +252,9 @@ export default async function ProductPage({ params }: ProductPageProps) {
const datasheetPath = getDatasheetPath(productSlug, locale);
const isFallback = (product.frontmatter as any).isFallback;
const categorySlug = slug[0];
const categoryKey = categorySlug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
const categoryTitle = t.has(`categories.${categoryKey}.title`) ? t(`categories.${categoryKey}.title`) : categorySlug;
const categoryFileSlug = await mapSlugToFileSlug(categorySlug, locale);
const categoryKey = categoryFileSlug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
const categoryTitle = t.has(`categories.${categoryKey}.title`) ? t(`categories.${categoryKey}.title`) : categoryFileSlug;
const sidebar = (
<ProductSidebar
@@ -305,9 +313,9 @@ export default async function ProductPage({ params }: ProductPageProps) {
</Badge>
))}
</div>
<h1 className="text-6xl md:text-8xl lg:text-9xl font-black text-white mb-8 tracking-tighter leading-[0.9] uppercase">
<Heading level={1} className="text-white mb-8 uppercase">
{product.frontmatter.title}
</h1>
</Heading>
<p className="text-xl md:text-2xl text-white/60 max-w-2xl leading-relaxed font-medium">
{product.frontmatter.description}
</p>
@@ -355,30 +363,51 @@ export default async function ProductPage({ params }: ProductPageProps) {
<MDXRemote source={processedContent} components={productComponents} />
</div>
{/* Datasheet Download Section - Only for Medium Voltage for now */}
{categoryFileSlug === 'medium-voltage-cables' && datasheetPath && (
<div className="mt-24 pt-24 border-t-2 border-neutral-dark/5">
<div className="mb-12">
<h2 className="text-3xl md:text-4xl font-black text-primary tracking-tighter uppercase mb-4">
{t('downloadDatasheet')}
</h2>
<div className="h-1.5 w-24 bg-accent rounded-full" />
</div>
<DatasheetDownload datasheetPath={datasheetPath} />
</div>
)}
{/* Structured Data */}
<Script
<JsonLd
id={`jsonld-${product.slug}`}
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Product',
name: product.frontmatter.title,
description: product.frontmatter.description,
sku: product.frontmatter.sku,
image: product.frontmatter.images?.[0] ? `https://klz-cables.com${product.frontmatter.images[0]}` : undefined,
brand: {
'@type': 'Brand',
name: 'KLZ Cables',
},
offers: {
'@type': 'Offer',
availability: 'https://schema.org/InStock',
priceCurrency: 'EUR',
url: `https://klz-cables.com/${locale}/products/${slug.join('/')}`,
},
}),
}}
data={{
'@context': 'https://schema.org',
'@type': 'Product',
name: product.frontmatter.title,
description: product.frontmatter.description,
sku: product.frontmatter.sku || product.slug.toUpperCase(),
image: product.frontmatter.images?.[0] ? `https://klz-cables.com${product.frontmatter.images[0]}` : undefined,
brand: {
'@type': 'Brand',
name: 'KLZ Cables',
},
offers: {
'@type': 'Offer',
availability: 'https://schema.org/InStock',
priceCurrency: 'EUR',
url: `https://klz-cables.com/${locale}/products/${slug.join('/')}`,
itemCondition: 'https://schema.org/NewCondition',
},
additionalProperty: technicalItems.map((item: any) => ({
'@type': 'PropertyValue',
name: item.label,
value: item.value,
})),
category: product.frontmatter.categories.join(', '),
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `https://klz-cables.com/${locale}/products/${slug.join('/')}`,
},
} as any}
/>
</div>
</div>

View File

@@ -0,0 +1,29 @@
import { ImageResponse } from 'next/og';
import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs';
export default async function Image({ params: { locale } }: { params: { locale: string } }) {
const t = await getTranslations({ locale, namespace: 'Products' });
const fonts = await getOgFonts();
const title = t('meta.title') || t('title');
const description = t('meta.description') || t('subtitle');
return new ImageResponse(
(
<OGImageTemplate
title={title}
description={description}
label="Products"
/>
),
{
...OG_IMAGE_SIZE,
fonts,
}
);
}

View File

@@ -1,11 +1,12 @@
import Reveal from '@/components/Reveal';
import Scribble from '@/components/Scribble';
import { Badge, Button, Card, Container, Section } from '@/components/ui';
import { Badge, Button, Card, Container, Heading, Section } from '@/components/ui';
import { getTranslations } from 'next-intl/server';
import { Metadata } from 'next';
import Image from 'next/image';
import Link from 'next/link';
import { mapFileSlugToTranslated } from '@/lib/slugs';
import { getOGImageMetadata } from '@/lib/metadata';
interface ProductsPageProps {
params: {
@@ -15,9 +16,11 @@ interface ProductsPageProps {
export async function generateMetadata({ params: { locale } }: ProductsPageProps): Promise<Metadata> {
const t = await getTranslations({ locale, namespace: 'Products' });
const title = t('meta.title') || t('title');
const description = t('meta.description') || t('subtitle');
return {
title: t('title'),
description: t('subtitle'),
title,
description,
alternates: {
canonical: `/${locale}/products`,
languages: {
@@ -27,14 +30,15 @@ export async function generateMetadata({ params: { locale } }: ProductsPageProps
},
},
openGraph: {
title: `${t('title')} | KLZ Cables`,
description: t('subtitle'),
title: `${title} | KLZ Cables`,
description,
url: `https://klz-cables.com/${locale}/products`,
images: getOGImageMetadata('products', title, locale),
},
twitter: {
card: 'summary_large_image',
title: `${t('title')} | KLZ Cables`,
description: t('subtitle'),
title: `${title} | KLZ Cables`,
description,
},
};
}
@@ -88,7 +92,7 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
<Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg px-3 py-1 md:px-4 md:py-1.5">
{t('heroSubtitle')}
</Badge>
<h1 className="text-4xl md:text-6xl lg:text-7xl font-extrabold text-white mb-4 md:mb-8 tracking-tight leading-[1.05]">
<Heading level={1} className="text-white mb-4 md:mb-8">
{t.rich('title', {
green: (chunks) => (
<span className="relative inline-block">
@@ -97,7 +101,7 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
</span>
)
})}
</h1>
</Heading>
<p className="text-lg md:text-xl text-white/70 leading-relaxed max-w-2xl mb-8 md:mb-12 line-clamp-2 md:line-clamp-none">
{t('subtitle')}
</p>
@@ -166,25 +170,27 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
</Section>
{/* Technical Support CTA */}
<Section className="bg-white py-12 md:py-28">
<Container>
<div className="bg-primary-dark rounded-[32px] md:rounded-[64px] p-6 md:p-20 lg:p-24 relative overflow-hidden">
<div className="absolute top-0 right-0 w-1/2 h-full bg-accent/5 -skew-x-12 translate-x-1/4" />
<div className="relative z-10 flex flex-col lg:flex-row items-center justify-between gap-6 md:gap-12">
<div className="max-w-2xl text-center lg:text-left">
<h2 className="text-2xl md:text-5xl lg:text-6xl font-bold text-white mb-4 md:mb-8 tracking-tight">{t('cta.title')}</h2>
<p className="text-base md:text-xl text-white/70 leading-relaxed">
{t('cta.description')}
</p>
<Reveal>
<Section className="bg-white py-12 md:py-28">
<Container>
<div className="bg-primary-dark rounded-[32px] md:rounded-[64px] p-6 md:p-20 lg:p-24 relative overflow-hidden">
<div className="absolute top-0 right-0 w-1/2 h-full bg-accent/5 -skew-x-12 translate-x-1/4" />
<div className="relative z-10 flex flex-col lg:flex-row items-center justify-between gap-6 md:gap-12">
<div className="max-w-2xl text-center lg:text-left">
<h2 className="text-2xl md:text-5xl lg:text-6xl font-bold text-white mb-4 md:mb-8 tracking-tight">{t('cta.title')}</h2>
<p className="text-base md:text-xl text-white/70 leading-relaxed">
{t('cta.description')}
</p>
</div>
<Button href={`/${params.locale}/contact`} variant="accent" size="lg" className="group whitespace-nowrap w-full md:w-auto md:h-16 md:px-10 md:text-xl">
{t('cta.button')}
<span className="ml-4 transition-transform group-hover:translate-x-2">&rarr;</span>
</Button>
</div>
<Button href={`/${params.locale}/contact`} variant="accent" size="lg" className="group whitespace-nowrap w-full md:w-auto md:h-16 md:px-10 md:text-xl">
{t('cta.button')}
<span className="ml-4 transition-transform group-hover:translate-x-2">&rarr;</span>
</Button>
</div>
</div>
</Container>
</Section>
</Container>
</Section>
</Reveal>
</div>
);
}

View File

@@ -0,0 +1,29 @@
import { ImageResponse } from 'next/og';
import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs';
export default async function Image({ params: { locale } }: { params: { locale: string } }) {
const t = await getTranslations({ locale, namespace: 'Team' });
const fonts = await getOgFonts();
const title = t('meta.title') || t('hero.subtitle');
const description = t('meta.description') || t('hero.title');
return new ImageResponse(
(
<OGImageTemplate
title={title}
description={description}
label="Our Team"
/>
),
{
...OG_IMAGE_SIZE,
fonts,
}
);
}

View File

@@ -1,11 +1,12 @@
import { useTranslations } from 'next-intl';
import { getTranslations } from 'next-intl/server';
import { Metadata } from 'next';
import JsonLd from '@/components/JsonLd';
import { getBreadcrumbSchema, SITE_URL } from '@/lib/schema';
import { Section, Container, Heading, Badge, Button } from '@/components/ui';
import { getOGImageMetadata } from '@/lib/metadata';
import Image from 'next/image';
import Reveal from '@/components/Reveal';
import Gallery from '@/components/team/Gallery';
interface TeamPageProps {
params: {
@@ -15,9 +16,11 @@ interface TeamPageProps {
export async function generateMetadata({ params: { locale } }: TeamPageProps): Promise<Metadata> {
const t = await getTranslations({ locale, namespace: 'Team' });
const title = t('meta.title') || t('hero.subtitle');
const description = t('meta.description') || t('hero.title');
return {
title: t('hero.subtitle'),
description: t('hero.title'),
title,
description,
alternates: {
canonical: `/${locale}/team`,
languages: {
@@ -27,20 +30,21 @@ export async function generateMetadata({ params: { locale } }: TeamPageProps): P
},
},
openGraph: {
title: `${t('hero.subtitle')} | KLZ Cables`,
description: t('hero.title'),
title: `${title} | KLZ Cables`,
description,
url: `https://klz-cables.com/${locale}/team`,
images: getOGImageMetadata('team', title, locale),
},
twitter: {
card: 'summary_large_image',
title: `${t('hero.subtitle')} | KLZ Cables`,
description: t('hero.title'),
title: `${title} | KLZ Cables`,
description,
},
};
}
export default function TeamPage() {
const t = useTranslations('Team');
export default async function TeamPage({ params: { locale } }: TeamPageProps) {
const t = await getTranslations({ locale, namespace: 'Team' });
return (
<div className="flex flex-col min-h-screen bg-neutral-light">
@@ -85,28 +89,30 @@ export default function TeamPage() {
}}
/>
{/* Hero Section */}
<section className="relative flex items-center justify-center overflow-hidden bg-primary-dark pt-32 pb-24 md:pt-[14%] md:pb-[12%]">
<div className="absolute inset-0 z-0">
<Image
src="/uploads/2024/12/DSC07655-Large.webp"
alt="KLZ Team"
fill
className="object-cover scale-105 animate-slow-zoom opacity-30 md:opacity-40"
priority
/>
<div className="absolute inset-0 bg-gradient-to-b from-primary-dark/80 via-primary-dark/40 to-primary-dark/80" />
</div>
<Container className="relative z-10 text-center text-white max-w-5xl animate-slide-up">
<Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg">{t('hero.badge')}</Badge>
<h1 className="text-3xl md:text-6xl lg:text-7xl font-extrabold tracking-tight leading-[1.1] mb-4 md:mb-8">
{t('hero.subtitle')}
</h1>
<p className="text-lg md:text-2xl text-white/70 font-medium italic">
{t('hero.title')}
</p>
</Container>
</section>
<Reveal>
<section className="relative flex items-center justify-center overflow-hidden bg-primary-dark pt-32 pb-24 md:pt-[14%] md:pb-[12%]">
<div className="absolute inset-0 z-0">
<Image
src="/uploads/2024/12/DSC07655-Large.webp"
alt="KLZ Team"
fill
className="object-cover scale-105 animate-slow-zoom opacity-30 md:opacity-40"
priority
/>
<div className="absolute inset-0 bg-gradient-to-b from-primary-dark/80 via-primary-dark/40 to-primary-dark/80" />
</div>
<Container className="relative z-10 text-center text-white max-w-5xl">
<Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg">{t('hero.badge')}</Badge>
<Heading level={1} className="text-white mb-4 md:mb-8">
{t('hero.subtitle')}
</Heading>
<p className="text-lg md:text-2xl text-white/70 font-medium italic">
{t('hero.title')}
</p>
</Container>
</section>
</Reveal>
{/* Michael Bodemer Section - Sticky Narrative Split Layout */}
<section className="relative bg-white overflow-hidden">
@@ -138,73 +144,75 @@ export default function TeamPage() {
</Button>
</div>
</Reveal>
<div className="w-full lg:w-1/2 relative min-h-[400px] md:min-h-[600px] lg:min-h-screen overflow-hidden order-1 lg:order-2">
<Image
src="/uploads/2024/12/DSC07768-Large.webp"
alt={t('michael.name')}
fill
<Reveal className="w-full lg:w-1/2 relative min-h-[400px] md:min-h-[600px] lg:min-h-screen overflow-hidden order-1 lg:order-2">
<Image
src="/uploads/2024/12/DSC07768-Large.webp"
alt={t('michael.name')}
fill
className="object-cover scale-105 hover:scale-100 transition-transform duration-1000"
sizes="(max-width: 1024px) 100vw, 50vw"
/>
<div className="absolute inset-0 bg-gradient-to-t from-primary-dark/60 lg:bg-gradient-to-r lg:from-primary-dark/20 to-transparent" />
</div>
</Reveal>
</div>
</section>
{/* Legacy Section - Immersive Background */}
<section className="relative py-16 md:py-48 bg-primary-dark text-white overflow-hidden">
<div className="absolute inset-0 z-0">
<Image
src="/uploads/2024/12/1694273920124-copy.webp"
alt={t('legacy.subtitle')}
fill
className="object-cover opacity-20 md:opacity-30 scale-110 animate-slow-zoom"
sizes="100vw"
/>
<div className="absolute inset-0 bg-primary-dark/60 mix-blend-multiply" />
</div>
<Container className="relative z-10">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 md:gap-16 items-center">
<div className="lg:col-span-6">
<Heading level={2} subtitle={t('legacy.subtitle')} className="text-white mb-6 md:mb-10">
<span className="text-white">{t('legacy.title')}</span>
</Heading>
<div className="space-y-4 md:space-y-8 text-base md:text-2xl text-white/80 leading-relaxed font-medium">
<p className="border-l-4 border-accent pl-5 md:pl-8 py-2 bg-white/5 backdrop-blur-sm rounded-r-xl md:rounded-r-2xl">
{t('legacy.p1')}
</p>
<p className="pl-6 md:pl-9 line-clamp-3 md:line-clamp-none">
{t('legacy.p2')}
</p>
</div>
</div>
<div className="lg:col-span-6 grid grid-cols-2 md:grid-cols-2 gap-3 md:gap-6">
<div className="p-4 md:p-8 bg-white/5 backdrop-blur-md border border-white/10 rounded-2xl md:rounded-[32px] hover:bg-white/10 transition-colors">
<div className="text-xl md:text-4xl font-extrabold text-accent mb-1 md:mb-2">{t('legacy.expertise')}</div>
<div className="text-[10px] md:text-sm font-bold uppercase tracking-widest text-white/50">{t('legacy.expertiseDesc')}</div>
</div>
<div className="p-4 md:p-8 bg-white/5 backdrop-blur-md border border-white/10 rounded-2xl md:rounded-[32px] hover:bg-white/10 transition-colors">
<div className="text-xl md:text-4xl font-extrabold text-accent mb-1 md:mb-2">{t('legacy.network')}</div>
<div className="text-[10px] md:text-sm font-bold uppercase tracking-widest text-white/50">{t('legacy.networkDesc')}</div>
</div>
</div>
<Reveal>
<section className="relative py-16 md:py-48 bg-primary-dark text-white overflow-hidden">
<div className="absolute inset-0 z-0">
<Image
src="/uploads/2024/12/1694273920124-copy.webp"
alt={t('legacy.subtitle')}
fill
className="object-cover opacity-20 md:opacity-30 scale-110 animate-slow-zoom"
sizes="100vw"
/>
<div className="absolute inset-0 bg-primary-dark/60 mix-blend-multiply" />
</div>
</Container>
</section>
<Container className="relative z-10">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 md:gap-16 items-center">
<div className="lg:col-span-6">
<Heading level={2} subtitle={t('legacy.subtitle')} className="text-white mb-6 md:mb-10">
<span className="text-white">{t('legacy.title')}</span>
</Heading>
<div className="space-y-4 md:space-y-8 text-base md:text-2xl text-white/80 leading-relaxed font-medium">
<p className="border-l-4 border-accent pl-5 md:pl-8 py-2 bg-white/5 backdrop-blur-sm rounded-r-xl md:rounded-r-2xl">
{t('legacy.p1')}
</p>
<p className="pl-6 md:pl-9 line-clamp-3 md:line-clamp-none">
{t('legacy.p2')}
</p>
</div>
</div>
<div className="lg:col-span-6 grid grid-cols-2 md:grid-cols-2 gap-3 md:gap-6">
<div className="p-4 md:p-8 bg-white/5 backdrop-blur-md border border-white/10 rounded-2xl md:rounded-[32px] hover:bg-white/10 transition-colors">
<div className="text-xl md:text-4xl font-extrabold text-accent mb-1 md:mb-2">{t('legacy.expertise')}</div>
<div className="text-[10px] md:text-sm font-bold uppercase tracking-widest text-white/50">{t('legacy.expertiseDesc')}</div>
</div>
<div className="p-4 md:p-8 bg-white/5 backdrop-blur-md border border-white/10 rounded-2xl md:rounded-[32px] hover:bg-white/10 transition-colors">
<div className="text-xl md:text-4xl font-extrabold text-accent mb-1 md:mb-2">{t('legacy.network')}</div>
<div className="text-[10px] md:text-sm font-bold uppercase tracking-widest text-white/50">{t('legacy.networkDesc')}</div>
</div>
</div>
</div>
</Container>
</section>
</Reveal>
{/* Klaus Mintel Section - Reversed Split Layout */}
<section className="relative bg-white overflow-hidden">
<div className="flex flex-col lg:flex-row">
<div className="w-full lg:w-1/2 relative min-h-[400px] md:min-h-[600px] lg:min-h-screen overflow-hidden order-1">
<Image
src="/uploads/2024/12/DSC07963-Large.webp"
alt={t('klaus.name')}
fill
<Reveal className="w-full lg:w-1/2 relative min-h-[400px] md:min-h-[600px] lg:min-h-screen overflow-hidden order-1">
<Image
src="/uploads/2024/12/DSC07963-Large.webp"
alt={t('klaus.name')}
fill
className="object-cover scale-105 hover:scale-100 transition-transform duration-1000"
sizes="(max-width: 1024px) 100vw, 50vw"
/>
<div className="absolute inset-0 bg-gradient-to-t from-white/60 lg:bg-gradient-to-l lg:from-primary-dark/20 to-transparent" />
</div>
</Reveal>
<Reveal className="w-full lg:w-1/2 p-6 md:p-24 lg:p-32 flex flex-col justify-center bg-neutral-light text-saturated relative order-2">
<div className="absolute top-0 left-0 w-32 h-full bg-saturated/5 skew-x-12 -translate-x-1/2" />
<div className="relative z-10">
@@ -273,29 +281,9 @@ export default function TeamPage() {
</Container>
</Section>
{/* Gallery Section - Premium Treatment */}
<Section className="bg-primary-dark py-16 md:py-32">
<Container>
<Heading level={2} subtitle={t('gallery.subtitle')} align="center" className="text-white mb-12 md:mb-20">
<span className="text-white">{t('gallery.title')}</span>
</Heading>
<div className="grid grid-cols-2 md:grid-cols-5 gap-3 md:gap-6">
{[
'/uploads/2024/12/DSC07539-Large-600x400.webp',
'/uploads/2024/12/DSC07460-Large-600x400.webp',
'/uploads/2024/12/DSC07469-Large-600x400.webp',
'/uploads/2024/12/DSC07433-Large-600x400.webp',
'/uploads/2024/12/DSC07387-Large-600x400.webp'
].map((src, idx) => (
<div key={idx} className="relative aspect-[3/4] rounded-2xl md:rounded-[32px] overflow-hidden group shadow-2xl">
<Image src={src} alt={t('gallery.title')} fill className="object-cover transition-transform duration-1000 group-hover:scale-110" />
<div className="absolute inset-0 bg-primary-dark/40 group-hover:bg-transparent transition-all duration-500" />
<div className="absolute inset-0 border-0 group-hover:border-[8px] md:group-hover:border-[12px] border-white/10 transition-all duration-500 pointer-events-none" />
</div>
))}
</div>
</Container>
</Section>
<Reveal>
<Gallery />
</Reveal>
</div>
);
}

View File

@@ -1,22 +1,53 @@
"use server";
import client, { ensureAuthenticated } from "@/lib/directus";
import { createItem } from "@directus/sdk";
import { sendEmail } from "@/lib/mail/mailer";
import ContactEmail from "@/components/emails/ContactEmail";
import React from "react";
import * as Sentry from "@sentry/nextjs";
import { getServerAppServices } from "@/lib/services/create-services.server";
export async function sendContactFormAction(formData: FormData) {
const services = getServerAppServices();
const logger = services.logger.child({ action: 'sendContactFormAction' });
const name = formData.get("name") as string;
const email = formData.get("email") as string;
const message = formData.get("message") as string;
const productName = formData.get("productName") as string | null;
if (!name || !email || !message) {
logger.warn('Missing required fields in contact form', { name: !!name, email: !!email, message: !!message });
return { success: false, error: "Missing required fields" };
}
const subject = productName
? `Product Inquiry: ${productName}`
// 1. Save to Directus
try {
await ensureAuthenticated();
if (productName) {
await client.request(createItem('product_requests', {
product_name: productName,
email,
message
}));
logger.info('Product request stored in Directus');
} else {
await client.request(createItem('contact_submissions', {
name,
email,
message
}));
logger.info('Contact submission stored in Directus');
}
} catch (error) {
logger.error('Failed to store submission in Directus', { error });
// We continue anyway to try sending the email, but maybe we should report this
}
// 2. Send Email
logger.info('Sending contact form email', { email, productName });
const subject = productName
? `Product Inquiry: ${productName}`
: "New Contact Form Submission";
const result = await sendEmail({
@@ -30,5 +61,12 @@ export async function sendContactFormAction(formData: FormData) {
}),
});
if (result.success) {
logger.info('Contact form email sent successfully', { messageId: result.messageId });
} else {
logger.error('Failed to send contact form email', { error: result.error });
services.errors.captureException(result.error, { action: 'sendContactFormAction', email });
}
return result;
}

View File

@@ -1,5 +1,9 @@
import { getServerAppServices } from '@/lib/services/create-services.server';
export const dynamic = 'force-dynamic';
export async function GET() {
const services = getServerAppServices();
services.logger.debug('Health check requested');
return new Response('OK', { status: 200 });
}

View File

@@ -2,11 +2,17 @@ import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: ['/api/', '/health/'],
},
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/api/', '/health/'],
},
{
userAgent: ['GPTBot', 'ClaudeBot', 'PerplexityBot', 'Google-Extended', 'OAI-SearchBot'],
allow: '/',
}
],
sitemap: 'https://klz-cables.com/sitemap.xml',
};
}

View File

@@ -16,21 +16,23 @@ export default function ContactForm() {
setStatus('submitting');
const formData = new FormData(e.currentTarget);
const email = formData.get('email') as string;
try {
const result = await sendContactFormAction(formData);
if (result.success) {
trackEvent('contact_form_submission', {
form_type: 'general',
email: formData.get('email') as string,
email,
});
setStatus('success');
(e.target as HTMLFormElement).reset();
} else {
console.error('Contact form submission failed:', { email, error: result.error });
setStatus('error');
}
} catch (error) {
console.error('Form submission error:', error);
console.error('Contact form submission error:', { email, error });
setStatus('error');
}
}
@@ -56,6 +58,29 @@ export default function ContactForm() {
);
}
if (status === 'error') {
return (
<Card className="p-6 md:p-12 rounded-2xl md:rounded-[40px] border-destructive/20 shadow-2xl text-center bg-destructive/5 animate-slide-up">
<div className="w-20 h-20 bg-destructive rounded-full flex items-center justify-center mx-auto mb-6 shadow-lg shadow-destructive/20">
<svg className="w-10 h-10 text-destructive-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10" />
<line x1="15" y1="9" x2="9" y2="15" strokeLinecap="round" strokeLinejoin="round" />
<line x1="9" y1="9" x2="15" y2="15" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<Heading level={3} className="mb-4 text-destructive font-black">
{t('form.errorTitle') || 'Submission Failed!'}
</Heading>
<p className="text-destructive/80 text-lg mb-8 leading-relaxed font-medium">
{t('form.error') || 'Something went wrong. Please check your input and try again.'}
</p>
<Button onClick={() => setStatus('idle')} variant="saturated" size="lg" className="w-full bg-destructive hover:bg-destructive/90 text-destructive-foreground border-destructive shadow-lg shadow-destructive/20">
{t('form.tryAgain') || 'Try Again'}
</Button>
</Card>
);
}
return (
<Card className="p-6 md:p-12 rounded-2xl md:rounded-[40px] border-none shadow-2xl animate-slide-up">
<Heading level={3} subtitle={t('form.subtitle')} className="mb-6 md:mb-10">
@@ -98,11 +123,6 @@ export default function ContactForm() {
required
/>
</div>
{status === 'error' && (
<div className="md:col-span-2 text-red-500 text-sm font-bold">
{t('form.error') || 'An error occurred. Please try again later.'}
</div>
)}
<div className="md:col-span-2 pt-2 md:pt-4">
<Button
type="submit"

View File

@@ -0,0 +1,68 @@
'use client';
import { cn } from '@/components/ui/utils';
import { useTranslations } from 'next-intl';
interface DatasheetDownloadProps {
datasheetPath: string;
className?: string;
}
export default function DatasheetDownload({ datasheetPath, className }: DatasheetDownloadProps) {
const t = useTranslations('Products');
return (
<div className={cn("mt-8 animate-slight-fade-in-from-bottom", className)}>
<a
href={datasheetPath}
target="_blank"
rel="noopener noreferrer"
className="group relative block w-full overflow-hidden rounded-[32px] bg-primary-dark p-1 transition-all duration-500 hover:shadow-[0_20px_50px_rgba(0,0,0,0.2)] hover:-translate-y-1"
>
{/* Animated Background Gradient */}
<div className="absolute inset-0 bg-gradient-to-r from-accent via-saturated to-accent opacity-20 group-hover:opacity-40 transition-opacity duration-500 animate-gradient-x" />
{/* Inner Content */}
<div className="relative flex items-center gap-6 rounded-[31px] bg-primary-dark/90 backdrop-blur-xl p-6 md:p-8 border border-white/10">
{/* Icon Container */}
<div className="relative flex h-16 w-16 flex-shrink-0 items-center justify-center rounded-2xl bg-white/5 border border-white/10 group-hover:bg-accent group-hover:border-white/20 transition-all duration-500">
<div className="absolute inset-0 rounded-2xl bg-accent/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<svg
className="relative h-8 w-8 text-white transition-transform duration-500 group-hover:scale-110 group-hover:rotate-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
{/* Text Content */}
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-accent">PDF Datasheet</span>
</div>
<h3 className="text-xl md:text-2xl font-black text-white uppercase tracking-tighter leading-none group-hover:text-accent transition-colors duration-300">
{t('downloadDatasheet')}
</h3>
<p className="mt-2 text-sm font-medium text-white/60 leading-relaxed">
{t('downloadDatasheetDesc')}
</p>
</div>
{/* Arrow Icon */}
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-white/5 text-white/20 group-hover:bg-accent group-hover:text-white group-hover:translate-x-1 transition-all duration-500">
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</a>
</div>
);
}

View File

@@ -31,7 +31,7 @@ export default function Footer() {
{t('tagline')}
</p>
<div className="flex gap-4">
<a href="https://www.linkedin.com/company/klz-vertriebs-gmbh/" target="_blank" rel="noopener noreferrer" className="w-12 h-12 rounded-full bg-white/5 flex items-center justify-center hover:bg-accent hover:text-primary-dark transition-all duration-300 border border-white/10">
<a href="https://www.linkedin.com/company/klz-vertriebs-gmbh/" target="_blank" rel="noopener noreferrer" className="w-12 h-12 rounded-full bg-white/5 flex items-center justify-center text-white hover:bg-accent hover:text-primary-dark transition-all duration-300 border border-white/10">
<span className="sr-only">LinkedIn</span>
<svg className="w-5 h-5 fill-current" viewBox="0 0 24 24">
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/>
@@ -43,35 +43,49 @@ export default function Footer() {
{/* Links Columns */}
<div className="lg:col-span-2">
<h4 className="text-accent font-bold uppercase tracking-widest text-xs md:text-sm mb-8">{t('legal')}</h4>
<ul className="space-y-4 text-white/70">
<li><Link href={`/${locale}/${t('legalNoticeSlug')}`} className="hover:text-white transition-colors">{t('legalNotice')}</Link></li>
<li><Link href={`/${locale}/${t('privacyPolicySlug')}`} className="hover:text-white transition-colors">{t('privacyPolicy')}</Link></li>
<li><Link href={`/${locale}/${t('termsSlug')}`} className="hover:text-white transition-colors">{t('terms')}</Link></li>
<ul className="space-y-4 text-white/70 list-none m-0 p-0">
<li><Link href={`/${locale}/${t('legalNoticeSlug')}`} className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block">{t('legalNotice')}</Link></li>
<li><Link href={`/${locale}/${t('privacyPolicySlug')}`} className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block">{t('privacyPolicy')}</Link></li>
<li><Link href={`/${locale}/${t('termsSlug')}`} className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block">{t('terms')}</Link></li>
</ul>
</div>
<div className="lg:col-span-2">
<h4 className="text-accent font-bold uppercase tracking-widest text-xs md:text-sm mb-8">{t('company')}</h4>
<ul className="space-y-4 text-white/70">
<li><Link href={`/${locale}/team`} className="hover:text-white transition-colors">{navT('team')}</Link></li>
<li><Link href={`/${locale}/products`} className="hover:text-white transition-colors">{navT('products')}</Link></li>
<li><Link href={`/${locale}/blog`} className="hover:text-white transition-colors">{navT('blog')}</Link></li>
<li><Link href={`/${locale}/contact`} className="hover:text-white transition-colors">{navT('contact')}</Link></li>
<ul className="space-y-4 text-white/70 list-none m-0 p-0">
<li><Link href={`/${locale}/team`} className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block">{navT('team')}</Link></li>
<li><Link href={`/${locale}/products`} className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block">{navT('products')}</Link></li>
<li><Link href={`/${locale}/blog`} className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block">{navT('blog')}</Link></li>
<li><Link href={`/${locale}/contact`} className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block">{navT('contact')}</Link></li>
</ul>
</div>
{/* Recent Posts Column */}
<div className="lg:col-span-4">
<h4 className="text-accent font-bold uppercase tracking-widest text-xs md:text-sm mb-8">{t('recentPosts')}</h4>
<ul className="space-y-6">
<ul className="space-y-6 list-none m-0 p-0">
{[
"Focus on wind farm construction: three typical cable challenges",
"Why the N2XS(F)2Y is the ideal cable for your energy project"
{
title: locale === 'de'
? "Windparkbau im Fokus: drei typische Kabelherausforderungen"
: "Focus on wind farm construction: three typical cable challenges",
slug: locale === 'de'
? "windparkbau-im-fokus-drei-typische-kabelherausforderungen"
: "focus-on-wind-farm-construction-three-typical-cable-challenges"
},
{
title: locale === 'de'
? "Warum das N2XS(F)2Y das ideale Kabel für Ihr Energieprojekt ist"
: "Why the N2XS(F)2Y is the ideal cable for your energy project",
slug: locale === 'de'
? "n2xsf2y-mittelspannungskabel-energieprojekt"
: "why-the-n2xsf2y-is-the-ideal-cable-for-your-energy-project"
}
].map((post, i) => (
<li key={i}>
<Link href="#" className="group block">
<Link href={`/${locale}/blog/${post.slug}`} className="group block text-white/80">
<p className="text-white/80 font-bold group-hover:text-accent transition-colors leading-snug mb-2 text-base md:text-base">
{post}
{post.title}
</p>
<span className="text-xs text-white/40 uppercase tracking-widest">{t('readArticle')} &rarr;</span>
</Link>
@@ -84,8 +98,8 @@ export default function Footer() {
<div className="pt-12 border-t border-white/10 flex flex-col md:flex-row justify-between items-center gap-8 text-white/40 text-xs md:text-sm font-medium">
<p>{t('copyright', { year: currentYear })}</p>
<div className="flex gap-8">
<Link href="/en" className="hover:text-white transition-colors">English</Link>
<Link href="/de" className="hover:text-white transition-colors">Deutsch</Link>
<Link href="/en" locale="en" className="hover:text-white transition-colors">English</Link>
<Link href="/de" locale="de" className="hover:text-white transition-colors">Deutsch</Link>
</div>
</div>
</Container>

View File

@@ -2,6 +2,7 @@
import Link from 'next/link';
import Image from 'next/image';
import { motion } from 'framer-motion';
import { useTranslations } from 'next-intl';
import { usePathname } from 'next/navigation';
import { Button } from './ui';
@@ -58,7 +59,7 @@ export default function Header() {
];
const headerClass = cn(
"fixed top-0 left-0 right-0 z-50 transition-all duration-500 safe-area-p",
"fixed top-0 left-0 right-0 z-50 transition-all duration-500 safe-area-p transform-gpu",
{
"bg-transparent py-4 md:py-8": isHomePage && !isScrolled && !isMobileMenuOpen,
"bg-primary py-3 md:py-4 shadow-2xl": !isHomePage || isScrolled || isMobileMenuOpen,
@@ -70,79 +71,168 @@ export default function Header() {
return (
<>
<header className={headerClass}>
<motion.header
className={headerClass}
initial={{ y: -100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.8, ease: "easeOut" }}
>
<div className="container mx-auto px-4 md:px-12 lg:px-16 max-w-7xl flex items-center justify-between">
<Link href={`/${currentLocale}`} className="flex-shrink-0 group touch-target">
<Image
src={logoSrc}
alt={t('home')}
width={120}
height={120}
className="h-10 md:h-14 w-auto transition-all duration-500 group-hover:scale-110"
priority
unoptimized
/>
</Link>
<motion.div
className="flex-shrink-0 group touch-target"
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.6, ease: "easeOut", delay: 0.1 }}
>
<Link href={`/${currentLocale}`}>
<Image
src={logoSrc}
alt={t('home')}
width={120}
height={120}
className="h-10 md:h-14 w-auto transition-all duration-500 group-hover:scale-110"
priority
unoptimized
/>
</Link>
</motion.div>
<div className="flex items-center gap-4 md:gap-12">
<nav className="hidden lg:flex items-center space-x-10">
{menuItems.map((item) => (
<Link
key={item.href}
href={`/${currentLocale}${item.href === '/' ? '' : item.href}`}
className={cn(
textColorClass,
"hover:text-accent font-bold transition-all text-base md:text-lg tracking-tight relative group"
)}
<motion.div
className="flex items-center gap-4 md:gap-12"
initial="hidden"
animate="visible"
variants={{
visible: {
transition: {
staggerChildren: 0.08,
delayChildren: 0.3
}
}
}}
>
<motion.nav
className="hidden lg:flex items-center space-x-10"
variants={navVariants}
>
{menuItems.map((item, idx) => (
<motion.div
key={item.href}
variants={navLinkVariants}
>
{item.label}
<span className="absolute -bottom-2 left-0 w-0 h-1 bg-accent transition-all duration-300 group-hover:w-full rounded-full" />
</Link>
<Link
href={`/${currentLocale}${item.href === '/' ? '' : item.href}`}
className={cn(
textColorClass,
"hover:text-accent font-bold transition-all duration-500 text-base md:text-lg tracking-tight relative group inline-block hover:-translate-y-0.5"
)}
>
{item.label}
<span className="absolute -bottom-2 left-0 w-0 h-1 bg-accent transition-all duration-500 group-hover:w-full rounded-full shadow-[0_0_12px_rgba(130,237,32,0.6)]" />
</Link>
</motion.div>
))}
</nav>
</motion.nav>
<div className={cn("hidden lg:flex items-center space-x-8", textColorClass)}>
<div className="flex items-center space-x-4 text-xs md:text-sm font-extrabold tracking-widest uppercase">
<Link
href={getPathForLocale('en')}
className={`hover:text-accent transition-colors flex items-center gap-2 touch-target ${currentLocale === 'en' ? 'text-accent' : 'opacity-60'}`}
>
EN
</Link>
<div className="w-px h-4 bg-current opacity-20" />
<Link
href={getPathForLocale('de')}
className={`hover:text-accent transition-colors flex items-center gap-2 touch-target ${currentLocale === 'de' ? 'text-accent' : 'opacity-60'}`}
>
DE
</Link>
</div>
<Button
href={`/${currentLocale}/contact`}
variant="white"
size="md"
className="px-8 shadow-xl"
<motion.div
className={cn("hidden lg:flex items-center space-x-8", textColorClass)}
variants={headerRightVariants}
>
<motion.div
className="flex items-center space-x-4 text-xs md:text-sm font-extrabold tracking-widest uppercase"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.6 }}
>
{t('contact')}
</Button>
</div>
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4, delay: 0.65 }}
>
<Link
href={getPathForLocale('en')}
className={`hover:text-accent transition-colors flex items-center gap-2 touch-target ${currentLocale === 'en' ? 'text-accent' : 'opacity-60'}`}
>
EN
</Link>
</motion.div>
<motion.div
className="w-px h-4 bg-current opacity-20"
initial={{ scaleY: 0 }}
animate={{ scaleY: 1 }}
transition={{ duration: 0.4, delay: 0.7 }}
/>
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4, delay: 0.75 }}
>
<Link
href={getPathForLocale('de')}
className={`hover:text-accent transition-colors flex items-center gap-2 touch-target ${currentLocale === 'de' ? 'text-accent' : 'opacity-60'}`}
>
DE
</Link>
</motion.div>
</motion.div>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.6, type: "spring", stiffness: 400, delay: 0.7 }}
>
<Button
href={`/${currentLocale}/contact`}
variant="white"
size="md"
className="px-8 shadow-xl"
>
{t('contact')}
</Button>
</motion.div>
</motion.div>
{/* Mobile Menu Button */}
<button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className={cn("lg:hidden touch-target p-2 rounded-xl bg-white/10 backdrop-blur-md border border-white/20 z-50", textColorClass)}
<motion.button
className={cn("lg:hidden touch-target p-2 rounded-xl bg-white/10 border border-white/20 z-50", textColorClass)}
aria-label={t('toggleMenu')}
initial={{ scale: 0.8, opacity: 0, rotate: -180 }}
animate={{ scale: 1, opacity: 1, rotate: 0 }}
transition={{ duration: 0.6, type: "spring", stiffness: 300, damping: 20, delay: 0.5 }}
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
>
<svg className="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<motion.svg
className="w-7 h-7"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ duration: 0.3, delay: 0.6 }}
>
{isMobileMenuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
<motion.path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.4, delay: 0.7 }}
/>
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
<motion.path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.4, delay: 0.7 }}
/>
)}
</svg>
</button>
</div>
</motion.svg>
</motion.button>
</motion.div>
</div>
{/* Mobile Menu Overlay */}
@@ -150,58 +240,156 @@ export default function Header() {
"fixed inset-0 bg-primary z-40 lg:hidden transition-all duration-500 ease-in-out flex flex-col",
isMobileMenuOpen ? "opacity-100 translate-y-0" : "opacity-0 -translate-y-full pointer-events-none"
)}>
<div className="flex-grow flex flex-col justify-center items-center p-8 space-y-8">
<motion.div
className="flex-grow flex flex-col justify-center items-center p-8 space-y-8"
initial="closed"
animate={isMobileMenuOpen ? "open" : "closed"}
variants={{
open: {
transition: {
staggerChildren: 0.1,
delayChildren: 0.2
}
}
}}
>
{menuItems.map((item, idx) => (
<Link
key={item.href}
href={`/${currentLocale}${item.href === '/' ? '' : item.href}`}
className={cn(
"text-2xl md:text-3xl font-extrabold text-white hover:text-accent transition-all transform",
isMobileMenuOpen ? "translate-y-0 opacity-100" : "translate-y-10 opacity-0"
)}
style={{ transitionDelay: `${idx * 100}ms` }}
<motion.div
key={item.href}
variants={{
closed: { opacity: 0, y: 50, scale: 0.9 },
open: {
opacity: 1,
y: 0,
scale: 1,
transition: {
duration: 0.6,
ease: "easeOut",
delay: idx * 0.08
}
}
}}
>
{item.label}
</Link>
<Link
href={`/${currentLocale}${item.href === '/' ? '' : item.href}`}
className="text-2xl md:text-3xl font-extrabold text-white hover:text-accent transition-all transform block py-4"
>
{item.label}
</Link>
</motion.div>
))}
<div className={cn(
"pt-8 border-t border-white/10 w-full flex flex-col items-center space-y-8 transition-all duration-500 delay-500",
isMobileMenuOpen ? "translate-y-0 opacity-100" : "translate-y-10 opacity-0"
)}>
<div className="flex items-center space-x-8 text-lg md:text-xl font-extrabold tracking-widest uppercase text-white">
<Link
href={getPathForLocale('en')}
className={`hover:text-accent transition-colors ${currentLocale === 'en' ? 'text-accent' : 'opacity-60'}`}
>
EN
</Link>
<div className="w-px h-6 bg-white/20" />
<Link
href={getPathForLocale('de')}
className={`hover:text-accent transition-colors ${currentLocale === 'de' ? 'text-accent' : 'opacity-60'}`}
>
DE
</Link>
</div>
<Button
href={`/${currentLocale}/contact`}
variant="accent"
size="lg"
className="w-full max-w-xs py-6 text-lg md:text-xl shadow-2xl"
<motion.div
className="pt-8 border-t border-white/10 w-full flex flex-col items-center space-y-8"
initial={{ opacity: 0, y: 30 }}
animate={isMobileMenuOpen ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 }}
transition={{ duration: 0.5, delay: 0.8 }}
>
<motion.div
className="flex items-center space-x-8 text-lg md:text-xl font-extrabold tracking-widest uppercase text-white"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4, delay: 0.9 }}
>
{t('contact')}
</Button>
</div>
</div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3, delay: 1.0 }}
>
<Link
href={getPathForLocale('en')}
className={`hover:text-accent transition-colors ${currentLocale === 'en' ? 'text-accent' : 'opacity-60'}`}
>
EN
</Link>
</motion.div>
<motion.div
className="w-px h-6 bg-white/20"
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.4, delay: 1.05 }}
/>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3, delay: 1.1 }}
>
<Link
href={getPathForLocale('de')}
className={`hover:text-accent transition-colors ${currentLocale === 'de' ? 'text-accent' : 'opacity-60'}`}
>
DE
</Link>
</motion.div>
</motion.div>
<motion.div
initial={{ scale: 0.9, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 400, damping: 20, delay: 1.2 }}
>
<Button
href={`/${currentLocale}/contact`}
variant="accent"
size="lg"
className="w-full max-w-xs py-6 text-lg md:text-xl shadow-2xl"
>
{t('contact')}
</Button>
</motion.div>
</motion.div>
{/* Bottom Branding */}
<div className="p-12 flex justify-center opacity-20">
<Image src="/logo-white.svg" alt={t('home')} width={80} height={80} unoptimized />
</div>
<motion.div
className="p-12 flex justify-center opacity-20"
initial={{ opacity: 0, scale: 0.8 }}
animate={isMobileMenuOpen ? { opacity: 0.2, scale: 1 } : { opacity: 0, scale: 0.8 }}
transition={{ duration: 0.5, delay: 1.4 }}
>
<motion.div
initial={{ scale: 0.5 }}
animate={{ scale: 1 }}
transition={{ type: "spring", stiffness: 300, delay: 1.5 }}
>
<Image src="/logo-white.svg" alt={t('home')} width={80} height={80} unoptimized />
</motion.div>
</motion.div>
</motion.div>
</div>
</header>
</motion.header>
</>
);
}
const navVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.06,
delayChildren: 0.1
}
}
} as const;
const navLinkVariants = {
hidden: { opacity: 0, y: 20, scale: 0.9 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: {
duration: 0.5,
ease: "easeOut"
}
}
} as const;
const headerRightVariants = {
hidden: { opacity: 0, x: 30 },
visible: {
opacity: 1,
x: 0,
transition: { duration: 0.6, ease: "easeOut" }
}
} as const;

View File

@@ -1,16 +1,51 @@
import Script from 'next/script';
import { Thing, WithContext } from 'schema-dts';
interface JsonLdProps {
id?: string;
data: any;
data?: WithContext<Thing> | WithContext<Thing>[];
}
export default function JsonLd({ id, data }: JsonLdProps) {
// If data is provided, use it. Otherwise, use the default Organization + WebSite schema.
const schemaData = data || [
{
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'KLZ Cables',
url: 'https://klz-cables.com',
logo: 'https://klz-cables.com/logo-blue.svg',
sameAs: [
'https://www.linkedin.com/company/klz-cables',
],
description: 'Premium Cable Solutions for Renewable Energy and Infrastructure.',
address: {
'@type': 'PostalAddress',
addressCountry: 'DE',
},
},
{
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'KLZ Cables',
url: 'https://klz-cables.com',
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: 'https://klz-cables.com/search?q={search_term_string}',
},
'query-input': 'required name=search_term_string',
},
}
];
return (
<Script
id={id || `jsonld-${Math.random().toString(36).substr(2, 9)}`}
<script
id={id}
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
dangerouslySetInnerHTML={{
__html: JSON.stringify(schemaData),
}}
/>
);
}

45
components/LeafletMap.tsx Normal file
View File

@@ -0,0 +1,45 @@
'use client';
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
// Fix for default marker icon in Leaflet with Next.js
const DefaultIcon = L.icon({
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
});
L.Marker.prototype.options.icon = DefaultIcon;
interface LeafletMapProps {
address: string;
lat: number;
lng: number;
}
export default function LeafletMap({ address, lat, lng }: LeafletMapProps) {
const position: [number, number] = [lat, lng];
return (
<MapContainer
center={position}
zoom={15}
scrollWheelZoom={false}
className="h-full w-full z-0"
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={position}>
<Popup>
<div className="text-primary font-bold">KLZ Cables</div>
<div className="text-sm whitespace-pre-line">{address}</div>
</Popup>
</Marker>
</MapContainer>
);
}

204
components/Lightbox.tsx Normal file
View File

@@ -0,0 +1,204 @@
'use client';
import React, { useEffect, useState, useCallback } from 'react';
import Image from 'next/image';
import { createPortal } from 'react-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
interface LightboxProps {
isOpen: boolean;
images: string[];
initialIndex: number;
onClose: () => void;
}
export default function Lightbox({ isOpen, images, initialIndex, onClose }: LightboxProps) {
const router = useRouter();
const searchParams = useSearchParams();
const pathname = usePathname();
const [currentIndex, setCurrentIndex] = useState(initialIndex);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
return () => setMounted(false);
}, []);
const updateUrl = useCallback((index: number | null) => {
const params = new URLSearchParams(searchParams.toString());
if (index !== null) {
params.set('photo', index.toString());
} else {
params.delete('photo');
}
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
}, [pathname, router, searchParams]);
const prevImage = useCallback(() => {
setCurrentIndex((prev) => {
const next = prev === 0 ? images.length - 1 : prev - 1;
updateUrl(next);
return next;
});
}, [images.length, updateUrl]);
const nextImage = useCallback(() => {
setCurrentIndex((prev) => {
const next = prev === images.length - 1 ? 0 : prev + 1;
updateUrl(next);
return next;
});
}, [images.length, updateUrl]);
useEffect(() => {
const photoParam = searchParams.get('photo');
if (photoParam !== null) {
const index = parseInt(photoParam, 10);
if (!isNaN(index) && index >= 0 && index < images.length) {
setCurrentIndex(index);
}
}
}, [searchParams, images.length]);
useEffect(() => {
if (isOpen) {
updateUrl(currentIndex);
}
}, [isOpen, currentIndex, updateUrl]);
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') handleClose();
if (e.key === 'ArrowLeft') prevImage();
if (e.key === 'ArrowRight') nextImage();
};
// Lock scroll
const originalStyle = window.getComputedStyle(document.body).overflow;
document.body.style.overflow = 'hidden';
window.addEventListener('keydown', handleKeyDown);
return () => {
document.body.style.overflow = originalStyle;
window.removeEventListener('keydown', handleKeyDown);
};
}, [isOpen, prevImage, nextImage]);
if (!mounted) return null;
const handleClose = () => {
updateUrl(null);
onClose();
};
return createPortal(
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-[99999] flex items-center justify-center">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
className="absolute inset-0 bg-primary/95 backdrop-blur-xl"
onClick={handleClose}
/>
<motion.button
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.5 }}
transition={{ delay: 0.1, duration: 0.4 }}
onClick={handleClose}
className="absolute top-6 right-6 text-white/60 hover:text-white transition-all duration-500 z-[10000] rounded-full w-14 h-14 flex items-center justify-center hover:bg-white/5 group border border-white/10"
aria-label="Close lightbox"
>
<div className="relative w-full h-full flex items-center justify-center group-hover:rotate-90 transition-transform duration-500">
<span className="text-3xl font-extralight leading-none mb-1">×</span>
</div>
</motion.button>
<motion.button
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ delay: 0.2, duration: 0.4 }}
onClick={prevImage}
className="absolute left-6 top-1/2 -translate-y-1/2 text-white/60 hover:text-white transition-all duration-500 w-14 h-14 flex items-center justify-center hover:bg-white/5 rounded-full z-[10000] group border border-white/10"
aria-label="Previous image"
>
<span className="text-4xl font-extralight group-hover:-translate-x-1 transition-transform duration-500"></span>
</motion.button>
<motion.button
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ delay: 0.2, duration: 0.4 }}
onClick={nextImage}
className="absolute right-6 top-1/2 -translate-y-1/2 text-white/60 hover:text-white transition-all duration-500 w-14 h-14 flex items-center justify-center hover:bg-white/5 rounded-full z-[10000] group border border-white/10"
aria-label="Next image"
>
<span className="text-4xl font-extralight group-hover:translate-x-1 transition-transform duration-500"></span>
</motion.button>
<motion.div
initial={{ opacity: 0, y: 40, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.98 }}
transition={{ duration: 0.6, ease: [0.25, 0.46, 0.45, 0.94] }}
className="relative w-full h-full max-w-6xl max-h-[85vh] flex flex-col items-center justify-center p-4 md:p-12 z-20 pointer-events-none"
>
<div className="pointer-events-auto w-full h-full flex flex-col items-center justify-center">
<div className="relative w-full h-full shadow-[0_40px_100px_-20px_rgba(0,0,0,0.6)] ring-1 ring-white/20 overflow-hidden bg-primary-dark/50 rounded-2xl flex items-center justify-center">
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={currentIndex}
initial={{ opacity: 0, scale: 1.1, filter: 'blur(10px)' }}
animate={{ opacity: 1, scale: 1, filter: 'blur(0px)' }}
exit={{ opacity: 0, scale: 0.9, filter: 'blur(10px)' }}
transition={{ duration: 0.7, ease: [0.25, 0.46, 0.45, 0.94] }}
className="relative w-full h-full"
>
<Image
src={images[currentIndex]}
alt={`Gallery image ${currentIndex + 1}`}
fill
className="object-cover transition-transform duration-1000 hover:scale-[1.03]"
unoptimized
/>
</motion.div>
</AnimatePresence>
{/* Technical Detail: Subtle grid overlay to reinforce industrial precision */}
<div className="absolute inset-0 pointer-events-none opacity-[0.03] bg-[url('/grid.svg')] bg-repeat z-10" />
{/* Premium Reflection: Subtle gradient to give material feel */}
<div className="absolute inset-0 pointer-events-none bg-gradient-to-tr from-white/10 via-transparent to-transparent opacity-40 z-10" />
</div>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ delay: 0.3, duration: 0.4 }}
className="mt-8 flex items-center gap-4"
>
<div className="h-px w-12 bg-white/20" />
<div className="bg-white/5 backdrop-blur-2xl text-white px-6 py-2 rounded-full border border-white/10 text-[11px] font-bold tracking-[0.2em] uppercase">
{currentIndex + 1} <span className="text-accent mx-3">/</span> {images.length}
</div>
<div className="h-px w-12 bg-white/20" />
</motion.div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>,
document.body
);
}

View File

@@ -0,0 +1,185 @@
import React from 'react';
interface OGImageTemplateProps {
title: string;
description?: string;
label?: string;
image?: string;
mode?: 'dark' | 'light' | 'image';
}
export function OGImageTemplate({
title,
description,
label,
image,
mode = 'dark',
}: OGImageTemplateProps) {
const primaryBlue = '#001a4d';
const accentGreen = '#82ed20';
const saturatedBlue = '#011dff';
const containerStyle: React.CSSProperties = {
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'center',
backgroundColor: mode === 'light' ? '#ffffff' : primaryBlue,
padding: '80px',
position: 'relative',
fontFamily: 'Inter',
};
return (
<div style={containerStyle}>
{/* Background Image with Overlay */}
{image && (
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
}}
>
<img
src={image}
alt=""
width="1200"
height="630"
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
}}
/>
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'linear-gradient(to right, rgba(0,26,77,0.95), rgba(0,26,77,0.6))',
}}
/>
</div>
)}
{/* Decorative Brand Accent (Top Right) */}
<div
style={{
position: 'absolute',
top: '-150px',
right: '-150px',
width: '600px',
height: '600px',
borderRadius: '300px',
backgroundColor: `${accentGreen}15`,
display: 'flex',
}}
/>
<div style={{ display: 'flex', flexDirection: 'column', position: 'relative', zIndex: 10 }}>
{/* Label / Category */}
{label && (
<div
style={{
fontSize: '24px',
fontWeight: 700,
color: accentGreen,
textTransform: 'uppercase',
letterSpacing: '0.3em',
marginBottom: '32px',
display: 'flex',
}}
>
{label}
</div>
)}
{/* Title */}
<div
style={{
fontSize: title.length > 40 ? '64px' : '82px',
fontWeight: 700,
color: 'white',
lineHeight: '1.05',
maxWidth: '950px',
marginBottom: '40px',
display: 'flex',
letterSpacing: '-0.02em',
}}
>
{title}
</div>
{/* Description */}
{description && (
<div
style={{
fontSize: '32px',
color: 'rgba(255,255,255,0.7)',
maxWidth: '850px',
lineHeight: '1.4',
display: 'flex',
fontWeight: 400,
}}
>
{description.length > 160 ? description.substring(0, 157) + '...' : description}
</div>
)}
</div>
{/* Brand Footer */}
<div
style={{
position: 'absolute',
bottom: '80px',
left: '80px',
display: 'flex',
alignItems: 'center',
}}
>
<div
style={{
width: '80px',
height: '6px',
backgroundColor: accentGreen,
borderRadius: '3px',
marginRight: '24px',
}}
/>
<div
style={{
fontSize: '24px',
fontWeight: 700,
color: 'white',
textTransform: 'uppercase',
letterSpacing: '0.15em',
display: 'flex',
}}
>
KLZ Cables
</div>
</div>
{/* Saturated Blue Brand Strip */}
<div
style={{
position: 'absolute',
top: 0,
right: 0,
width: '12px',
height: '100%',
backgroundColor: saturatedBlue,
}}
/>
</div>
);
}

View File

@@ -3,6 +3,7 @@
import Image from 'next/image';
import { useTranslations } from 'next-intl';
import RequestQuoteForm from '@/components/RequestQuoteForm';
import DatasheetDownload from '@/components/DatasheetDownload';
import Scribble from '@/components/Scribble';
import { cn } from '@/components/ui/utils';
@@ -64,33 +65,7 @@ export default function ProductSidebar({ productName, productImage, datasheetPat
{/* Datasheet Download */}
{datasheetPath && (
<a
href={datasheetPath}
target="_blank"
rel="noopener noreferrer"
className="block bg-white rounded-2xl border border-neutral-medium overflow-hidden group transition-all duration-500 hover:shadow-xl hover:border-saturated/30 hover:-translate-y-0.5"
>
<div className="p-4 flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-neutral-medium/20 flex items-center justify-center flex-shrink-0 group-hover:bg-saturated group-hover:text-white transition-all duration-500 text-saturated border border-transparent group-hover:border-white/20">
<svg className="w-6 h-6 transition-transform duration-500 group-hover:scale-110" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm md:text-base font-heading font-black text-neutral-dark m-0 uppercase tracking-tighter leading-tight group-hover:text-saturated transition-colors duration-300">
{t('downloadDatasheet')}
</h3>
<p className="text-text-secondary text-[10px] md:text-xs m-0 mt-0.5 font-semibold leading-tight truncate uppercase tracking-widest opacity-60">
{t('downloadDatasheetDesc')}
</p>
</div>
<div className="text-neutral-dark/20 group-hover:text-saturated transition-all duration-500 transform group-hover:translate-x-1">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</a>
<DatasheetDownload datasheetPath={datasheetPath} className="mt-0" />
)}
</div>
);

View File

@@ -59,7 +59,7 @@ export default function RequestQuoteForm({ productName }: RequestQuoteFormProps)
<p className="text-text-secondary text-xs leading-tight mb-4 !mt-0">
{t('successDesc', { productName })}
</p>
<button
<button
onClick={() => setStatus('idle')}
className="inline-flex items-center text-[9px] font-bold uppercase tracking-[0.2em] text-primary hover:text-accent transition-colors group"
>
@@ -71,6 +71,32 @@ export default function RequestQuoteForm({ productName }: RequestQuoteFormProps)
);
}
if (status === 'error') {
return (
<div className="bg-destructive/5 border border-destructive/20 text-destructive p-4 rounded-xl text-center animate-fade-in !mt-0">
<div className="w-10 h-10 bg-destructive rounded-full flex items-center justify-center mx-auto mb-3 shadow-lg shadow-destructive/20">
<svg className="w-5 h-5 text-destructive-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="3">
<circle cx="12" cy="12" r="10" />
<line x1="15" y1="9" x2="9" y2="15" />
<line x1="9" y1="9" x2="15" y2="15" />
</svg>
</div>
<h3 className="text-base font-extrabold mb-1 tracking-tight !mt-0 text-destructive">{t('errorTitle') || 'Submission Failed'}</h3>
<p className="text-destructive/80 text-xs leading-tight mb-4 !mt-0">
{t('errorDesc') || 'Something went wrong. Please try again.'}
</p>
<button
onClick={() => setStatus('idle')}
className="inline-flex items-center text-[9px] font-bold uppercase tracking-[0.2em] text-destructive hover:text-destructive/80 transition-colors group"
>
<span className="border-b-2 border-destructive/10 group-hover:border-destructive/30 transition-colors pb-1">
{t('tryAgain') || 'Try Again'}
</span>
</button>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="space-y-3 !mt-0">
<div className="space-y-2 !mt-0">

View File

@@ -49,7 +49,9 @@ export default function AnalyticsProvider() {
id="umami-analytics"
src="/stats/script.js"
data-website-id={websiteId}
data-host-url="/stats"
strategy="afterInteractive"
data-domains="klz-cables.com"
defer
/>
);

View File

@@ -0,0 +1,171 @@
import Link from 'next/link';
import VisualLinkPreview from '@/components/blog/VisualLinkPreview';
import { Callout } from '@/components/ui';
import HighlightBox from '@/components/blog/HighlightBox';
import Stats from '@/components/blog/Stats';
import AnimatedImage from '@/components/blog/AnimatedImage';
import ChatBubble from '@/components/blog/ChatBubble';
import SplitHeading from '@/components/blog/SplitHeading';
import PowerCTA from '@/components/blog/PowerCTA';
import StickyNarrative from '@/components/blog/StickyNarrative';
import TechnicalGrid from '@/components/blog/TechnicalGrid';
import ComparisonGrid from '@/components/blog/ComparisonGrid';
export const mdxComponents = {
VisualLinkPreview,
Callout,
HighlightBox,
Stats,
AnimatedImage,
ChatBubble,
PowerCTA,
SplitHeading,
StickyNarrative,
TechnicalGrid,
ComparisonGrid,
h1: () => null,
a: ({ href, children, ...props }: any) => {
// Special handling for PDF downloads to make them prominent
if (href?.endsWith('.pdf')) {
return (
<a
href={href}
{...props}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-3 px-6 py-3 bg-primary text-white font-bold rounded-xl hover:bg-accent hover:text-primary-dark transition-all duration-300 no-underline my-8 group shadow-lg hover:shadow-xl hover:-translate-y-1"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<span>{children}</span>
<span className="text-xs opacity-50 font-normal group-hover:opacity-100 transition-opacity">(PDF)</span>
</a>
);
}
if (href?.startsWith('/')) {
return (
<Link href={href} {...props} className="text-primary font-medium hover:underline decoration-2 underline-offset-2 transition-all">
{children}
</Link>
);
}
return (
<a
href={href}
{...props}
target="_blank"
rel="noopener noreferrer"
className="text-primary font-medium hover:underline decoration-2 underline-offset-2 transition-all inline-flex items-center gap-1"
>
{children}
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
);
},
img: (props: any) => (
<AnimatedImage src={props.src} alt={props.alt} />
),
h2: ({ children, ...props }: any) => {
const id = typeof children === 'string'
? children.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-')
: props.id;
return (
<SplitHeading {...props} id={id} className="mt-16 mb-6 pb-3 border-b-2 border-primary/20">
{children}
</SplitHeading>
);
},
h3: ({ children, ...props }: any) => {
const id = typeof children === 'string'
? children.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-')
: props.id;
return (
<h3 {...props} id={id} className="text-2xl font-bold text-text-primary mt-12 mb-4">
{children}
</h3>
);
},
p: ({ children, ...props }: any) => (
<p {...props} className="text-lg text-text-secondary leading-relaxed mb-6">
{children}
</p>
),
ul: ({ children, ...props }: any) => (
<ul {...props} className="my-8 space-y-3">
{children}
</ul>
),
ol: ({ children, ...props }: any) => (
<ol {...props} className="my-8 space-y-3 list-decimal list-inside">
{children}
</ol>
),
li: ({ children, ...props }: any) => (
<li {...props} className="text-lg text-text-secondary flex items-start gap-3">
<span className="text-primary mt-1.5 flex-shrink-0">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
</span>
<span className="flex-1">{children}</span>
</li>
),
blockquote: ({ children, ...props }: any) => (
<blockquote {...props} className="my-8 pl-6 border-l-4 border-primary bg-neutral-light/30 py-4 pr-6 rounded-r-lg">
<div className="text-lg text-text-primary italic">
{children}
</div>
</blockquote>
),
strong: ({ children, ...props }: any) => (
<strong {...props} className="font-bold text-primary">
{children}
</strong>
),
code: ({ children, ...props }: any) => (
<code {...props} className="px-2 py-1 bg-neutral-light text-primary rounded font-mono text-sm">
{children}
</code>
),
pre: ({ children, ...props }: any) => (
<pre {...props} className="my-8 p-6 bg-neutral-dark/5 rounded-xl overflow-x-auto">
{children}
</pre>
),
table: ({ children, ...props }: any) => (
<div className="my-8 overflow-x-auto rounded-lg border border-neutral-200 shadow-sm">
<table {...props} className="w-full text-left text-sm text-text-secondary">
{children}
</table>
</div>
),
thead: ({ children, ...props }: any) => (
<thead {...props} className="bg-neutral-50 text-text-primary font-semibold border-b border-neutral-200">
{children}
</thead>
),
tbody: ({ children, ...props }: any) => (
<tbody {...props} className="divide-y divide-neutral-200 bg-white">
{children}
</tbody>
),
tr: ({ children, ...props }: any) => (
<tr {...props} className="hover:bg-neutral-50/50 transition-colors">
{children}
</tr>
),
th: ({ children, ...props }: any) => (
<th {...props} className="px-6 py-4 whitespace-nowrap">
{children}
</th>
),
td: ({ children, ...props }: any) => (
<td {...props} className="px-6 py-4">
{children}
</td>
),
};

View File

@@ -19,7 +19,7 @@ export default function TableOfContents({ headings, locale }: TableOfContentsPro
useEffect(() => {
const observerOptions = {
rootMargin: '-100px 0% -80% 0%',
rootMargin: '-10% 0% -70% 0%',
threshold: 0
};
@@ -31,12 +31,18 @@ export default function TableOfContents({ headings, locale }: TableOfContentsPro
});
}, observerOptions);
const elements = headings.map((h) => document.getElementById(h.id));
elements.forEach((el) => {
if (el) observer.observe(el);
});
// Use a small delay to ensure MDX content is rendered and IDs are present
const timer = setTimeout(() => {
const elements = headings.map((h) => document.getElementById(h.id));
elements.forEach((el) => {
if (el) observer.observe(el);
});
}, 500);
return () => observer.disconnect();
return () => {
clearTimeout(timer);
observer.disconnect();
};
}, [headings]);
if (headings.length === 0) return null;
@@ -75,7 +81,7 @@ export default function TableOfContents({ headings, locale }: TableOfContentsPro
}
}}
>
{heading.text}
{heading.text.replace(/\*\*(.*?)\*\*/g, '$1')}
</a>
</li>
))}

View File

@@ -1,10 +1,15 @@
import React from 'react';
'use client';
import React, { useState, useEffect } from 'react';
import Image from 'next/image';
import { useTranslations } from 'next-intl';
import { Section, Container, Heading } from '../../components/ui';
import Lightbox from '../Lightbox';
import { useSearchParams } from 'next/navigation';
export default function GallerySection() {
const t = useTranslations('Home.gallery');
const searchParams = useSearchParams();
const images = [
'/uploads/2024/12/DSC07433-Large-600x400.webp',
'/uploads/2024/12/DSC07460-Large-600x400.webp',
@@ -14,8 +19,22 @@ export default function GallerySection() {
'/uploads/2024/12/DSC07768-Large.webp',
];
const [lightboxOpen, setLightboxOpen] = useState(false);
const [lightboxIndex, setLightboxIndex] = useState(0);
useEffect(() => {
const photoParam = searchParams.get('photo');
if (photoParam !== null) {
const index = parseInt(photoParam, 10);
if (!isNaN(index) && index >= 0 && index < images.length) {
setLightboxIndex(index);
setLightboxOpen(true);
}
}
}, [searchParams, images.length]);
return (
<Section className="bg-white text-neutral-dark py-32">
<Section className="bg-white text-white py-32">
<Container>
<Heading level={2} subtitle={t('subtitle')} align="center">
{t('title')}
@@ -23,9 +42,16 @@ export default function GallerySection() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{images.map((src, idx) => (
<div key={idx} className="relative aspect-[4/3] overflow-hidden rounded-3xl group shadow-lg hover:shadow-2xl transition-all duration-700">
<Image
src={src}
<button
key={idx}
onClick={() => {
setLightboxIndex(idx);
setLightboxOpen(true);
}}
className="relative aspect-[4/3] overflow-hidden rounded-3xl group shadow-lg hover:shadow-2xl transition-all duration-700 cursor-pointer"
>
<Image
src={src}
alt={`${t('alt')} ${idx + 1}`}
fill
className="object-cover transition-transform duration-1000 group-hover:scale-110"
@@ -33,10 +59,17 @@ export default function GallerySection() {
/>
<div className="absolute inset-0 bg-primary-dark/20 group-hover:bg-transparent transition-all duration-500" />
<div className="absolute inset-0 border-0 group-hover:border-[16px] border-white/10 transition-all duration-500 pointer-events-none" />
</div>
</button>
))}
</div>
</Container>
<Lightbox
isOpen={lightboxOpen}
images={images}
initialIndex={lightboxIndex}
onClose={() => setLightboxOpen(false)}
/>
</Section>
);
}

View File

@@ -1,48 +1,168 @@
import React from 'react';
import { useTranslations } from 'next-intl';
import { Container, Button, Section, Heading } from '@/components/ui';
'use client';
import Scribble from '@/components/Scribble';
import { Button, Container, Heading, Section } from '@/components/ui';
import { motion } from 'framer-motion';
import { useTranslations } from 'next-intl';
import HeroIllustration from './HeroIllustration';
export default function Hero() {
const t = useTranslations('Home.hero');
return (
<Section className="relative h-[70vh] md:h-[90vh] flex items-center justify-center overflow-hidden bg-primary py-0 md:py-0 lg:py-0">
<HeroIllustration />
<Container className="relative z-10 text-left text-white w-full">
<div className="max-w-5xl animate-slide-up">
<Heading level={1} className="mb-4 md:mb-8 max-w-[15ch] md:max-w-none text-white">
{t.rich('title', {
green: (chunks) => (
<span className="relative inline-block">
<span className="relative z-10 text-accent italic">{chunks}</span>
<Scribble variant="circle" className="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block" />
</span>
)
})}
</Heading>
<p className="text-lg md:text-xl text-white leading-relaxed max-w-2xl mb-8 md:mb-12 line-clamp-2 md:line-clamp-none">
{t('subtitle')}
</p>
<div className="flex flex-col md:flex-row gap-3 md:gap-6">
<Button href="/contact" variant="accent" size="lg" className="group w-full md:w-auto md:h-16 md:px-10 md:text-lg">
{t('cta')}
<span className="ml-3 transition-transform group-hover:translate-x-1">&rarr;</span>
</Button>
<Button href="/products" variant="ghost" size="lg" className="group w-full md:w-auto text-white hover:bg-white/10 md:bg-white md:text-saturated md:hover:bg-neutral-light md:h-16 md:px-10 md:text-lg">
{t('exploreProducts')}
</Button>
</div>
</div>
<Section className="relative min-h-[85vh] md:h-[90vh] flex flex-col items-center justify-center overflow-hidden bg-primary py-12 md:py-0 lg:py-0">
<Container className="relative z-10 text-center md:text-left text-white w-full order-2 md:order-none">
<motion.div
className="max-w-5xl mx-auto md:mx-0"
initial="hidden"
animate="visible"
variants={containerVariants}
>
<motion.div variants={headingVariants}>
<Heading level={1} className="text-center md:text-left mb-6 md:mb-8 md:max-w-none text-white text-4xl sm:text-5xl md:text-7xl font-extrabold [text-shadow:_-2px_-2px_0_#002b49,_2px_-2px_0_#002b49,_-2px_2px_0_#002b49,_2px_2px_0_#002b49,_-2px_0_0_#002b49,_2px_0_0_#002b49,_0_-2px_0_#002b49,_0_2px_0_#002b49]">
{t.rich('title', {
green: (chunks) => (
<span className="relative inline-block">
<motion.span
className="relative z-10 text-accent italic"
variants={accentVariants}
>
{chunks}
</motion.span>
<motion.div
variants={scribbleVariants}
className="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block absolute -z-10"
>
<Scribble variant="circle" />
</motion.div>
</span>
)
})}
</Heading>
</motion.div>
<motion.div variants={subtitleVariants}>
<p className="text-lg md:text-xl text-white/90 leading-relaxed max-w-2xl mb-10 md:mb-12">
{t('subtitle')}
</p>
</motion.div>
<motion.div
className="flex flex-col sm:flex-row justify-center md:justify-start gap-4 md:gap-6"
variants={buttonContainerVariants}
>
<motion.div variants={buttonVariants}>
<Button href="/contact" variant="accent" size="lg" className="group w-full sm:w-auto h-14 md:h-16 px-8 md:px-10 text-base md:text-lg">
{t('cta')}
<span className="transition-transform group-hover/btn:translate-x-1">&rarr;</span>
</Button>
</motion.div>
<motion.div variants={buttonVariants}>
<Button href="/products" variant="white" size="lg" className="group w-full sm:w-auto h-14 md:h-16 px-8 md:px-10 text-base md:text-lg md:bg-white md:text-primary md:hover:bg-neutral-light md:border-none">
{t('exploreProducts')}
</Button>
</motion.div>
</motion.div>
</motion.div>
</Container>
<motion.div
className="relative md:absolute inset-0 z-0 w-full h-[40vh] md:h-full order-1 md:order-none mb-[-80px] md:mb-0 mt-[80px] md:mt-0 overflow-visible pointer-events-none"
initial={{ opacity: 0, scale: 0.95, filter: 'blur(20px)' }}
animate={{ opacity: 1, scale: 1, filter: 'blur(0px)' }}
transition={{ duration: 2.2, ease: 'easeOut', delay: 0.05 }}
>
<HeroIllustration />
</motion.div>
<div className="absolute bottom-6 md:bottom-10 left-1/2 -translate-x-1/2 animate-bounce hidden sm:block">
<motion.div
className="absolute bottom-6 md:bottom-10 left-1/2 -translate-x-1/2 hidden sm:block"
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, ease: "easeOut", delay: 3 }}
>
<div className="w-6 h-10 border-2 border-white/30 rounded-full flex justify-center p-1">
<div className="w-1 h-2 bg-white rounded-full" />
<motion.div
className="w-1 h-2 bg-white rounded-full"
animate={{ y: [0, -10, 0] }}
transition={{
duration: 1.5,
repeat: Infinity,
ease: "easeInOut"
}}
/>
</div>
</div>
</motion.div>
</Section>
);
}
const containerVariants = {
hidden: { opacity: 1 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.12,
delayChildren: 0.4
}
}
} as const;
const headingVariants = {
hidden: { opacity: 0, y: 60, scale: 0.85 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { duration: 1.2, ease: [0.25, 0.46, 0.45, 0.94] }
}
} as const;
const accentVariants = {
hidden: { opacity: 0, scale: 0.9, rotate: -5 },
visible: {
opacity: 1,
scale: 1,
rotate: 0,
transition: { duration: 0.8, ease: [0.25, 0.46, 0.45, 0.94] }
}
} as const;
const scribbleVariants = {
hidden: { opacity: 0, scale: 0, rotate: 180 },
visible: {
opacity: 1,
scale: 1,
rotate: 0,
transition: { duration: 1, type: "spring", stiffness: 300, damping: 20 }
}
} as const;
const subtitleVariants = {
hidden: { opacity: 0, y: 40, scale: 0.95 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { duration: 1, ease: [0.25, 0.46, 0.45, 0.94] }
}
} as const;
const buttonContainerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15,
delayChildren: 0.4
}
}
} as const;
const buttonVariants = {
hidden: { opacity: 0, y: 30, scale: 0.9 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { type: "spring", stiffness: 400, damping: 20 }
}
} as const;

View File

@@ -163,11 +163,589 @@ const POWER_LINES = [
export default function HeroIllustration() {
return (
<div className="absolute inset-0 z-0 overflow-hidden bg-primary">
<div className="absolute md:inset-0 z-0 overflow-visible bg-primary w-full h-full">
<svg
viewBox="400 0 1000 1100"
className="w-full h-full opacity-60 md:opacity-100 scale-[1.44] md:scale-100 translate-x-0 overflow-visible md:hidden"
preserveAspectRatio="xMidYMid meet"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
{/* Electric energy flow gradient */}
<linearGradient id="energy-pulse" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#82ed20" stopOpacity="0" />
<stop offset="30%" stopColor="#82ed20" stopOpacity="0.6" />
<stop offset="50%" stopColor="#9bf14d" stopOpacity="1" />
<stop offset="70%" stopColor="#82ed20" stopOpacity="0.6" />
<stop offset="100%" stopColor="#82ed20" stopOpacity="0" />
</linearGradient>
{/* Wind flow gradient */}
<linearGradient id="wind-flow" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="white" stopOpacity="0" />
<stop offset="30%" stopColor="white" stopOpacity="0.4" />
<stop offset="50%" stopColor="white" stopOpacity="0.6" />
<stop offset="70%" stopColor="white" stopOpacity="0.4" />
<stop offset="100%" stopColor="white" stopOpacity="0" />
</linearGradient>
{/* Sun ray gradient */}
<linearGradient id="sun-ray" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#FFD700" stopOpacity="0.6" />
<stop offset="50%" stopColor="#FFD700" stopOpacity="0.3" />
<stop offset="100%" stopColor="#82ed20" stopOpacity="0.1" />
</linearGradient>
{/* Glow filter */}
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
{/* Soft glow for nodes */}
<filter id="soft-glow" x="-100%" y="-100%" width="300%" height="300%">
<feGaussianBlur stdDeviation="2" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
{/* Sun glow filter */}
<filter id="sun-glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="4" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
{/* Main scene container - positioned to the right */}
<g transform="translate(900, 100)">
{/* === ISOMETRIC GRID === */}
<g opacity="0.15">
{/* Horizontal grid lines (going from top-left to bottom-right) */}
{[...Array(GRID.rows + 1)].map((_, row) => {
const start = gridToScreen(0, row);
const end = gridToScreen(GRID.cols, row);
return (
<line
key={`h-${row}`}
x1={start.x}
y1={start.y}
x2={end.x}
y2={end.y}
stroke="white"
strokeWidth="1"
/>
);
})}
{/* Vertical grid lines (going from top-right to bottom-left) */}
{[...Array(GRID.cols + 1)].map((_, col) => {
const start = gridToScreen(col, 0);
const end = gridToScreen(col, GRID.rows);
return (
<line
key={`v-${col}`}
x1={start.x}
y1={start.y}
x2={end.x}
y2={end.y}
stroke="white"
strokeWidth="1"
/>
);
})}
</g>
{/* Grid intersection nodes */}
<g opacity="0.2">
{[...Array(GRID.cols + 1)].map((_, col) =>
[...Array(GRID.rows + 1)].map((_, row) => {
const pos = gridToScreen(col, row);
return (
<circle
key={`node-${col}-${row}`}
cx={pos.x}
cy={pos.y}
r="2"
fill="white"
/>
);
})
)}
</g>
{/* === POWER LINES (Base cables) === */}
<g stroke="white" strokeWidth="2" strokeOpacity="0.25">
{POWER_LINES.map((line, i) => {
const from = gridToScreen(line.from.col, line.from.row);
const to = gridToScreen(line.to.col, line.to.row);
return (
<line
key={`cable-${i}`}
x1={from.x}
y1={from.y}
x2={to.x}
y2={to.y}
/>
);
})}
</g>
{/* === ANIMATED ENERGY FLOW === */}
<g filter="url(#glow)">
{POWER_LINES.map((line, i) => {
const from = gridToScreen(line.from.col, line.from.row);
const to = gridToScreen(line.to.col, line.to.row);
const length = Math.sqrt(
Math.pow(to.x - from.x, 2) + Math.pow(to.y - from.y, 2)
);
return (
<line
key={`flow-${i}`}
x1={from.x}
y1={from.y}
x2={to.x}
y2={to.y}
stroke="url(#energy-pulse)"
strokeWidth="3"
strokeLinecap="round"
strokeDasharray={`${length * 0.2} ${length * 0.8}`}
>
<animate
attributeName="stroke-dashoffset"
from={length}
to={0}
dur={`${1.5 + (i % 3) * 0.5}s`}
repeatCount="indefinite"
/>
</line>
);
})}
</g>
{/* === SOLAR PANELS === */}
{INFRASTRUCTURE.solar.map((panel, i) => {
const pos = gridToScreen(panel.col, panel.row);
return (
<g key={`solar-${i}`} transform={`translate(${pos.x}, ${pos.y})`}>
{/* Panel base */}
<path
d="M -20 0 L 0 -10 L 20 0 L 0 10 Z"
fill="white"
fillOpacity="0.1"
stroke="white"
strokeWidth="1"
strokeOpacity="0.4"
/>
{/* Panel surface (tilted) */}
<path
d="M -15 -5 L 0 -15 L 15 -5 L 0 5 Z"
fill="white"
fillOpacity="0.15"
stroke="white"
strokeWidth="1"
strokeOpacity="0.5"
/>
{/* Panel grid lines */}
<line x1="-7" y1="-10" x2="7" y2="0" stroke="white" strokeWidth="0.5" strokeOpacity="0.3" />
<line x1="0" y1="-15" x2="0" y2="5" stroke="white" strokeWidth="0.5" strokeOpacity="0.3" />
{/* Connection glow */}
<circle r="4" fill="#82ed20" fillOpacity="0.4" filter="url(#soft-glow)">
<animate attributeName="fillOpacity" values="0.3;0.6;0.3" dur="2s" repeatCount="indefinite" />
</circle>
</g>
);
})}
{/* === WIND TURBINES === */}
{INFRASTRUCTURE.wind.map((turbine, i) => {
const pos = gridToScreen(turbine.col, turbine.row);
return (
<g key={`wind-${i}`} transform={`translate(${pos.x}, ${pos.y})`}>
{/* Base */}
<ellipse cx="0" cy="0" rx="10" ry="5" fill="white" fillOpacity="0.1" stroke="white" strokeWidth="1" strokeOpacity="0.3" />
{/* Tower */}
<line x1="0" y1="0" x2="0" y2="-60" stroke="white" strokeWidth="2" strokeOpacity="0.5" />
{/* Nacelle */}
<ellipse cx="0" cy="-60" rx="6" ry="3" fill="white" fillOpacity="0.3" stroke="white" strokeWidth="1" />
{/* Blades */}
<g transform="translate(0, -60)">
{[0, 120, 240].map((angle, j) => (
<line
key={`blade-${i}-${j}`}
x1="0"
y1="0"
x2="0"
y2="-30"
stroke="white"
strokeWidth="1.5"
strokeOpacity="0.6"
transform={`rotate(${angle})`}
>
<animateTransform
attributeName="transform"
type="rotate"
from={`${angle} 0 0`}
to={`${angle + 360} 0 0`}
dur={`${3 + i}s`}
repeatCount="indefinite"
/>
</line>
))}
<circle r="3" fill="white" fillOpacity="0.4" />
</g>
{/* Connection glow */}
<circle r="5" fill="#82ed20" fillOpacity="0.4" filter="url(#soft-glow)">
<animate attributeName="fillOpacity" values="0.3;0.6;0.3" dur="2.5s" repeatCount="indefinite" />
</circle>
</g>
);
})}
{/* === SUBSTATIONS === */}
{INFRASTRUCTURE.substations.map((sub, i) => {
const pos = gridToScreen(sub.col, sub.row);
const isCollection = sub.type === 'collection';
return (
<g key={`substation-${i}`} transform={`translate(${pos.x}, ${pos.y})`}>
{/* Base platform */}
<path
d="M -25 0 L 0 -12 L 25 0 L 0 12 Z"
fill="white"
fillOpacity="0.1"
stroke="white"
strokeWidth="1"
strokeOpacity="0.4"
/>
{/* Building */}
<path
d={isCollection
? "M -18 0 L -18 -20 L 0 -32 L 18 -20 L 18 0"
: "M -22 0 L -22 -25 L 0 -37 L 22 -25 L 22 0"
}
fill="white"
fillOpacity="0.08"
stroke="white"
strokeWidth="1"
strokeOpacity="0.5"
/>
{/* Equipment */}
<rect x="-10" y="-12" width="6" height="8" fill="white" fillOpacity="0.2" stroke="white" strokeWidth="0.5" />
<rect x="4" y="-12" width="6" height="8" fill="white" fillOpacity="0.2" stroke="white" strokeWidth="0.5" />
{/* Insulators */}
<line x1="-7" y1="-12" x2="-7" y2="-22" stroke="white" strokeWidth="1" strokeOpacity="0.4" />
<line x1="7" y1="-12" x2="7" y2="-22" stroke="white" strokeWidth="1" strokeOpacity="0.4" />
<circle cx="-7" cy="-22" r="2" fill="white" fillOpacity="0.4" />
<circle cx="7" cy="-22" r="2" fill="white" fillOpacity="0.4" />
{/* Connection glow */}
<circle r="8" fill="#82ed20" fillOpacity="0.3" filter="url(#soft-glow)">
<animate attributeName="r" values="6;10;6" dur="3s" repeatCount="indefinite" />
<animate attributeName="fillOpacity" values="0.2;0.5;0.2" dur="3s" repeatCount="indefinite" />
</circle>
</g>
);
})}
{/* === TRANSMISSION TOWERS === */}
{INFRASTRUCTURE.towers.map((tower, i) => {
const pos = gridToScreen(tower.col, tower.row);
return (
<g key={`tower-${i}`} transform={`translate(${pos.x}, ${pos.y})`}>
{/* Base */}
<ellipse cx="0" cy="0" rx="8" ry="4" fill="white" fillOpacity="0.1" stroke="white" strokeWidth="1" strokeOpacity="0.3" />
{/* Tower legs */}
<path d="M -6 0 L -3 -45 M 6 0 L 3 -45" stroke="white" strokeWidth="1.5" strokeOpacity="0.5" />
{/* Cross braces */}
<path d="M -5 -10 L 5 -10 M -4 -20 L 4 -20 M -3 -30 L 3 -30 M -3 -45 L 3 -45" stroke="white" strokeWidth="1" strokeOpacity="0.3" />
{/* Cross arms */}
<line x1="-12" y1="-40" x2="12" y2="-40" stroke="white" strokeWidth="1" strokeOpacity="0.4" />
<line x1="-10" y1="-32" x2="10" y2="-32" stroke="white" strokeWidth="1" strokeOpacity="0.4" />
{/* Insulators */}
<circle cx="-10" cy="-40" r="1.5" fill="white" fillOpacity="0.4" />
<circle cx="10" cy="-40" r="1.5" fill="white" fillOpacity="0.4" />
{/* Connection glow */}
<circle r="5" fill="#82ed20" fillOpacity="0.3" filter="url(#soft-glow)">
<animate attributeName="fillOpacity" values="0.2;0.5;0.2" dur="2s" repeatCount="indefinite" />
</circle>
</g>
);
})}
{/* === CITY BUILDINGS === */}
{INFRASTRUCTURE.city.map((building, i) => {
const pos = gridToScreen(building.col, building.row);
const heights = { tall: 70, medium: 45, small: 30 };
const height = heights[building.type as keyof typeof heights];
return (
<g key={`building-${i}`} transform={`translate(${pos.x}, ${pos.y})`}>
{/* Base */}
<path
d="M -12 0 L 0 -6 L 12 0 L 0 6 Z"
fill="white"
fillOpacity="0.1"
stroke="white"
strokeWidth="1"
strokeOpacity="0.3"
/>
{/* Building front */}
<path
d={`M -12 0 L -12 -${height} L 0 -${height + 6} L 0 -6 Z`}
fill="white"
fillOpacity="0.1"
stroke="white"
strokeWidth="1"
strokeOpacity="0.4"
/>
{/* Building side */}
<path
d={`M 0 -6 L 0 -${height + 6} L 12 -${height} L 12 0 Z`}
fill="white"
fillOpacity="0.05"
stroke="white"
strokeWidth="1"
strokeOpacity="0.3"
/>
{/* Windows */}
{[...Array(Math.floor(height / 15))].map((_, w) => (
<g key={`window-${i}-${w}`}>
<rect x="-9" y={-12 - w * 15} width="3" height="4" fill="white" fillOpacity="0.2" />
<rect x="-4" y={-12 - w * 15} width="3" height="4" fill="white" fillOpacity="0.2" />
<rect x="3" y={-15 - w * 15} width="3" height="4" fill="white" fillOpacity="0.15" />
<rect x="7" y={-15 - w * 15} width="3" height="4" fill="white" fillOpacity="0.15" />
</g>
))}
{/* Connection glow */}
<circle r="4" fill="#82ed20" fillOpacity="0.3" filter="url(#soft-glow)">
<animate attributeName="fillOpacity" values="0.2;0.5;0.2" dur={`${2 + i * 0.3}s`} repeatCount="indefinite" />
</circle>
</g>
);
})}
{/* === CITY 2 BUILDINGS (bottom-left) === */}
{INFRASTRUCTURE.city2.map((building, i) => {
const pos = gridToScreen(building.col, building.row);
const heights = { tall: 70, medium: 45, small: 30 };
const height = heights[building.type as keyof typeof heights];
return (
<g key={`building2-${i}`} transform={`translate(${pos.x}, ${pos.y})`}>
{/* Base */}
<path
d="M -12 0 L 0 -6 L 12 0 L 0 6 Z"
fill="white"
fillOpacity="0.1"
stroke="white"
strokeWidth="1"
strokeOpacity="0.3"
/>
{/* Building front */}
<path
d={`M -12 0 L -12 -${height} L 0 -${height + 6} L 0 -6 Z`}
fill="white"
fillOpacity="0.1"
stroke="white"
strokeWidth="1"
strokeOpacity="0.4"
/>
{/* Building side */}
<path
d={`M 0 -6 L 0 -${height + 6} L 12 -${height} L 12 0 Z`}
fill="white"
fillOpacity="0.05"
stroke="white"
strokeWidth="1"
strokeOpacity="0.3"
/>
{/* Windows */}
{[...Array(Math.floor(height / 15))].map((_, w) => (
<g key={`window2-${i}-${w}`}>
<rect x="-9" y={-12 - w * 15} width="3" height="4" fill="white" fillOpacity="0.2" />
<rect x="-4" y={-12 - w * 15} width="3" height="4" fill="white" fillOpacity="0.2" />
<rect x="3" y={-15 - w * 15} width="3" height="4" fill="white" fillOpacity="0.15" />
<rect x="7" y={-15 - w * 15} width="3" height="4" fill="white" fillOpacity="0.15" />
</g>
))}
{/* Connection glow */}
<circle r="4" fill="#82ed20" fillOpacity="0.3" filter="url(#soft-glow)">
<animate attributeName="fillOpacity" values="0.2;0.5;0.2" dur={`${2.5 + i * 0.3}s`} repeatCount="indefinite" />
</circle>
</g>
);
})}
{/* === TREES === */}
{INFRASTRUCTURE.trees.map((tree, i) => {
const pos = gridToScreen(tree.col, tree.row);
return (
<g key={`tree-${i}`} transform={`translate(${pos.x}, ${pos.y})`}>
{/* Trunk */}
<line x1="0" y1="0" x2="0" y2="-15" stroke="white" strokeWidth="2" strokeOpacity="0.3" />
{/* Foliage - layered circles for tree crown */}
<ellipse cx="0" cy="-22" rx="10" ry="8" fill="white" fillOpacity="0.12" stroke="white" strokeWidth="0.5" strokeOpacity="0.2" />
<ellipse cx="-5" cy="-26" rx="7" ry="6" fill="white" fillOpacity="0.1" stroke="white" strokeWidth="0.5" strokeOpacity="0.15" />
<ellipse cx="5" cy="-26" rx="7" ry="6" fill="white" fillOpacity="0.1" stroke="white" strokeWidth="0.5" strokeOpacity="0.15" />
<ellipse cx="0" cy="-30" rx="6" ry="5" fill="white" fillOpacity="0.08" stroke="white" strokeWidth="0.5" strokeOpacity="0.1" />
</g>
);
})}
{/* === ABSTRACT WIND EFFECTS === */}
{INFRASTRUCTURE.wind.map((turbine, i) => {
const pos = gridToScreen(turbine.col, turbine.row);
return (
<g key={`wind-effect-${i}`} transform={`translate(${pos.x}, ${pos.y})`}>
{/* Wind swoosh lines - curved paths flowing toward turbine */}
{[0, 1, 2].map((j) => (
<path
key={`wind-line-${i}-${j}`}
d={`M ${-80 - j * 15} ${-70 - j * 8} Q ${-50 - j * 10} ${-65 - j * 5} ${-20} ${-60}`}
stroke="url(#wind-flow)"
strokeWidth="2"
fill="none"
strokeLinecap="round"
opacity="0"
>
<animate
attributeName="opacity"
values="0;0.6;0"
dur={`${2 + j * 0.5}s`}
begin={`${j * 0.7 + i * 0.3}s`}
repeatCount="indefinite"
/>
<animate
attributeName="stroke-dashoffset"
from="100"
to="0"
dur={`${2 + j * 0.5}s`}
begin={`${j * 0.7 + i * 0.3}s`}
repeatCount="indefinite"
/>
</path>
))}
{/* Additional wind particles */}
{[0, 1, 2, 3].map((j) => (
<circle
key={`wind-particle-${i}-${j}`}
r="1.5"
fill="white"
opacity="0"
>
<animate
attributeName="cx"
values={`${-70 - j * 10};${-10}`}
dur={`${1.5 + j * 0.3}s`}
begin={`${j * 0.4 + i * 0.2}s`}
repeatCount="indefinite"
/>
<animate
attributeName="cy"
values={`${-75 - j * 5};${-60}`}
dur={`${1.5 + j * 0.3}s`}
begin={`${j * 0.4 + i * 0.2}s`}
repeatCount="indefinite"
/>
<animate
attributeName="opacity"
values="0;0.5;0"
dur={`${1.5 + j * 0.3}s`}
begin={`${j * 0.4 + i * 0.2}s`}
repeatCount="indefinite"
/>
</circle>
))}
</g>
);
})}
{/* === SCHEMATIC SUN RAYS === */}
{/* Simple downward rays above each solar panel */}
{INFRASTRUCTURE.solar.map((panel, i) => {
const pos = gridToScreen(panel.col, panel.row);
return (
<g key={`sun-ray-${i}`} transform={`translate(${pos.x}, ${pos.y})`}>
{/* Three short schematic rays coming down to panel */}
{[-8, 0, 8].map((offset, j) => (
<line
key={`ray-${i}-${j}`}
x1={offset}
y1={-45}
x2={offset * 0.3}
y2={-18}
stroke="#FFD700"
strokeWidth="1.5"
strokeOpacity="0.4"
strokeLinecap="round"
strokeDasharray="4 6"
>
<animate
attributeName="strokeOpacity"
values="0.2;0.5;0.2"
dur={`${2 + j * 0.3}s`}
begin={`${i * 0.2}s`}
repeatCount="indefinite"
/>
<animate
attributeName="stroke-dashoffset"
from="10"
to="0"
dur="1.5s"
repeatCount="indefinite"
/>
</line>
))}
</g>
);
})}
{/* === ENERGY PARTICLES === */}
{POWER_LINES.map((line, i) => {
const from = gridToScreen(line.from.col, line.from.row);
const to = gridToScreen(line.to.col, line.to.row);
return (
<circle
key={`particle-${i}`}
r="3"
fill="#82ed20"
filter="url(#soft-glow)"
>
<animate
attributeName="cx"
values={`${from.x};${to.x}`}
dur={`${1 + (i % 4) * 0.3}s`}
repeatCount="indefinite"
/>
<animate
attributeName="cy"
values={`${from.y};${to.y}`}
dur={`${1 + (i % 4) * 0.3}s`}
repeatCount="indefinite"
/>
<animate
attributeName="opacity"
values="0;0.8;0"
dur={`${1 + (i % 4) * 0.3}s`}
repeatCount="indefinite"
/>
</circle>
);
})}
</g>
</svg>
{/* Desktop SVG - Original */}
<svg
viewBox="-400 -200 1800 1100"
className="w-full h-full opacity-40 md:opacity-100 scale-150 md:scale-100 translate-x-1/4 md:translate-x-0"
preserveAspectRatio="xMidYMid slice"
className="w-full h-full opacity-100 scale-100 translate-x-0 hidden md:block"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
@@ -741,7 +1319,7 @@ export default function HeroIllustration() {
</g>
</svg>
<div className="absolute inset-0 bg-gradient-to-b from-primary/10 via-transparent to-primary/90" />
<div className="absolute inset-0 bg-gradient-to-b from-primary/10 via-transparent to-primary/90 pointer-events-none" />
</div>
);
}

View File

@@ -19,7 +19,7 @@ export default async function RecentPosts({ locale }: RecentPostsProps) {
<Section className="bg-neutral py-16 md:py-24">
<Container>
<div className="flex flex-col md:flex-row items-start md:items-end justify-between mb-12 md:mb-16 gap-6">
<Heading level={2} subtitle={t('latestNews')} className="mb-0">
<Heading level={2} subtitle={t('latestNews')} className="mb-0 text-primary">
{t('allArticles')}
</Heading>
<Link href={`/${locale}/blog`} className="group flex items-center text-primary font-bold text-base md:text-lg touch-target">

View File

@@ -6,12 +6,12 @@ export default function WhatWeDo() {
const t = useTranslations('Home.whatWeDo');
return (
<Section className="bg-white text-neutral-dark">
<Section className="bg-white">
<Container>
<div className="sticky-narrative-container">
<div className="sticky-narrative-sidebar">
<div className="lg:sticky lg:top-32">
<Heading level={2} subtitle={t('expertise')}>
<Heading level={2} subtitle={t('expertise')} className="text-primary-dark">
{t('title')}
</Heading>
<p className="text-base md:text-lg text-text-secondary leading-relaxed">
@@ -33,7 +33,7 @@ export default function WhatWeDo() {
</span>
<div className="h-px flex-grow bg-neutral-medium" />
</div>
<h3 className="text-lg md:text-xl font-bold mb-3 md:mb-4 text-saturated group-hover:text-accent-dark transition-colors">{t(`items.${idx}.title`)}</h3>
<h3 className="text-lg md:text-xl font-bold mb-3 md:mb-4 text-primary-dark group-hover:text-accent-dark transition-colors">{t(`items.${idx}.title`)}</h3>
<p className="text-text-secondary text-base md:text-base leading-relaxed">{t(`items.${idx}.description`)}</p>
</div>
))}

View File

@@ -6,12 +6,12 @@ export default function WhyChooseUs() {
const t = useTranslations('Home.whyChooseUs');
return (
<Section className="bg-neutral-light text-neutral-dark">
<Section className="bg-neutral-light">
<Container>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 lg:gap-24">
<div className="lg:col-span-4 order-1 lg:order-2">
<div className="sticky top-32">
<Heading level={2} subtitle={t('whyKlz')}>
<Heading level={2} subtitle={t('whyKlz')} className="text-primary-dark">
{t('title')}
</Heading>
<p className="text-base md:text-lg text-text-secondary leading-relaxed">
@@ -26,7 +26,7 @@ export default function WhyChooseUs() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
</div>
<span className="font-bold text-saturated text-base md:text-base">{t(`features.${i}`)}</span>
<span className="font-bold text-primary-dark text-base md:text-base">{t(`features.${i}`)}</span>
</div>
))}
</div>
@@ -36,9 +36,9 @@ export default function WhyChooseUs() {
{[0, 1, 2, 3].map((idx) => (
<div key={idx} className="p-10 bg-white rounded-3xl border border-neutral-medium hover:border-accent transition-all duration-500 hover:shadow-xl group">
<div className="w-14 h-14 bg-saturated/10 rounded-2xl flex items-center justify-center mb-8 group-hover:bg-accent transition-colors duration-500">
<span className="text-saturated font-bold text-lg group-hover:text-primary-dark">0{idx + 1}</span>
<span className="text-white font-bold text-lg group-hover:text-primary-dark">0{idx + 1}</span>
</div>
<h3 className="text-xl font-bold mb-4 text-saturated">{t(`items.${idx}.title`)}</h3>
<h3 className="text-xl font-bold mb-4 text-primary-dark">{t(`items.${idx}.title`)}</h3>
<p className="text-text-secondary text-base md:text-base leading-relaxed">{t(`items.${idx}.description`)}</p>
</div>
))}

View File

@@ -0,0 +1,55 @@
'use client';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import Image from 'next/image';
import Lightbox from '@/components/Lightbox';
import { Section, Container, Heading } from '@/components/ui';
export default function Gallery() {
const t = useTranslations('Team');
const [lightboxOpen, setLightboxOpen] = useState(false);
const [lightboxIndex, setLightboxIndex] = useState(0);
const teamGalleryImages = [
'/uploads/2024/12/DSC07539-Large-600x400.webp',
'/uploads/2024/12/DSC07460-Large-600x400.webp',
'/uploads/2024/12/DSC07469-Large-600x400.webp',
'/uploads/2024/12/DSC07433-Large-600x400.webp',
'/uploads/2024/12/DSC07387-Large-600x400.webp'
];
return (
<Section className="bg-primary-dark py-16 md:py-32">
<Container>
<Heading level={2} subtitle={t('gallery.subtitle')} align="center" className="text-white mb-12 md:mb-20">
<span className="text-white">{t('gallery.title')}</span>
</Heading>
<div className="grid grid-cols-2 md:grid-cols-5 gap-3 md:gap-6">
{teamGalleryImages.map((src, idx) => (
<button
key={idx}
onClick={() => {
setLightboxIndex(idx);
setLightboxOpen(true);
}}
className="relative aspect-[3/4] rounded-2xl md:rounded-[32px] overflow-hidden group shadow-2xl cursor-pointer"
>
<Image src={src} alt={t('gallery.title')} fill className="object-cover transition-transform duration-1000 group-hover:scale-110" />
<div className="absolute inset-0 bg-primary-dark/40 group-hover:bg-transparent transition-all duration-500" />
<div className="absolute inset-0 border-0 group-hover:border-[8px] md:group-hover:border-[12px] border-white/10 transition-all duration-500 pointer-events-none" />
</button>
))}
</div>
</Container>
<Lightbox
isOpen={lightboxOpen}
images={teamGalleryImages}
initialIndex={lightboxIndex}
onClose={() => setLightboxOpen(false)}
/>
</Section>
);
}

View File

@@ -11,16 +11,16 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
}
export function Button({ className, variant = 'primary', size = 'md', href, ...props }: ButtonProps) {
const baseStyles = 'inline-flex items-center justify-center rounded-full font-semibold transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none active:scale-95';
const baseStyles = 'inline-flex items-center justify-center rounded-full font-semibold transition-all duration-500 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none active:scale-95 hover:-translate-y-1 hover:scale-[1.02] relative overflow-hidden group/btn isolate';
const variants = {
primary: 'bg-primary text-white hover:bg-primary-dark shadow-md hover:shadow-lg',
secondary: 'bg-secondary text-white hover:bg-secondary-light shadow-md hover:shadow-lg',
accent: 'bg-accent text-primary-dark hover:bg-accent-dark shadow-md hover:shadow-lg',
saturated: 'bg-saturated text-white hover:bg-primary shadow-md hover:shadow-lg',
outline: 'border-2 border-primary bg-transparent hover:bg-primary hover:text-white text-primary',
ghost: 'hover:bg-primary-light text-primary',
white: 'bg-white text-primary hover:bg-neutral-light shadow-md hover:shadow-lg',
primary: 'bg-primary text-white shadow-md hover:shadow-primary/30 hover:shadow-2xl',
secondary: 'bg-secondary text-white shadow-md hover:shadow-secondary/30 hover:shadow-2xl',
accent: 'bg-accent text-primary-dark shadow-md hover:shadow-accent/30 hover:shadow-2xl',
saturated: 'bg-saturated text-white shadow-md hover:shadow-primary/30 hover:shadow-2xl',
outline: 'border-2 border-primary bg-transparent text-primary hover:text-white hover:shadow-primary/20 hover:shadow-xl',
ghost: 'text-primary hover:shadow-lg',
white: 'bg-white text-primary shadow-md hover:shadow-primary/30 hover:shadow-2xl hover:text-white',
};
const sizes = {
@@ -32,15 +32,42 @@ export function Button({ className, variant = 'primary', size = 'md', href, ...p
const styles = cn(baseStyles, variants[variant], sizes[size], className);
const overlayColors = {
primary: 'bg-primary-dark',
secondary: 'bg-secondary-light',
accent: 'bg-accent-dark',
saturated: 'bg-primary',
outline: 'bg-primary',
ghost: 'bg-primary-light/10',
white: 'bg-primary-light',
};
const content = (
<>
<span className={cn(
"relative z-10 flex items-center justify-center gap-2 transition-colors duration-500",
variant === 'white' ? "group-hover/btn:text-primary-dark" : ""
)}>
{props.children}
</span>
<span className={cn(
"absolute inset-0 translate-y-[101%] group-hover/btn:translate-y-0 transition-transform duration-500 ease-out",
overlayColors[variant]
)} />
</>
);
if (href) {
return (
<Link href={href} className={styles}>
{props.children}
{content}
</Link>
);
}
return (
<button className={styles} {...props} />
<button className={styles} {...props}>
{content}
</button>
);
}

View File

@@ -17,9 +17,9 @@ export function Heading({
const Tag = `h${level}` as any;
const sizes = {
1: 'text-3xl md:text-5xl lg:text-6xl font-extrabold leading-[1.1] tracking-tight',
2: 'text-2xl md:text-4xl lg:text-5xl font-bold leading-[1.2] tracking-tight',
3: 'text-xl md:text-2xl lg:text-3xl font-bold leading-[1.3] tracking-tight',
1: 'text-2xl md:text-4xl lg:text-5xl font-bold leading-[1.1] tracking-tight',
2: 'text-xl md:text-3xl lg:text-4xl font-bold leading-[1.2] tracking-tight',
3: 'text-lg md:text-2xl lg:text-3xl font-bold leading-[1.3] tracking-tight',
4: 'text-lg md:text-xl lg:text-2xl font-bold leading-[1.4]',
5: 'text-base md:text-lg font-bold leading-[1.5]',
6: 'text-base md:text-lg font-semibold leading-[1.6]',

View File

@@ -1,6 +1,6 @@
---
title: 'Windparkbau im Fokus: drei typische Kabelherausforderungen'
date: '2025-11-05T10:16:10'
date: '2025-01-05T10:16:10'
featuredImage: /uploads/2025/04/image_fx_-2025-02-20T193520.620.webp
locale: de
category: Kabel Technologie
@@ -81,11 +81,13 @@ title="Wann klagt der NABU gegen Windkraftprojekte?"
summary="45 Klagen wurden wegen Fehlplanungen bei Windenergie zwischen 2010 und 2019 vom NABU auf den Weg gebracht. Nicht weil der Windenergieausbau aufgehalten werden soll, sondern weil immer wieder Vorhaben und Planungen eklatant gegen Naturschutzrecht verstoßen."
image="https://www.nabu.de/imperia/md/nabu/images/umwelt/energie/energietraeger/windkraft/161125-nabu-windrad-allgaeu-heidrun-burchard.jpeg"
/>
<AnimatedImage src="/uploads/2025/02/image_fx_-7.webp" alt="Windpark Baustelle" width={1408} height={768} />
## Qualität und Nachhaltigkeit als Erfolgsfaktor
Neben Zeit und Logistik spielt auch die [**Kabelqualität**](https://www.windkraft-journal.de/2025/07/14/planungsempfehlung-bei-der-verkabelung-von-windparks-durch-wind-turbine-com/214028) eine entscheidende Rolle für die langfristige Performance eines **Windparks**. Schließlich sollen die installierten **[Mittelspannungs](/de/stromkabel/mittelspannungskabel/) und [Hochspannungskabel](/de/stromkabel/hochspannungskabel/)** über Jahrzehnte zuverlässig Energie übertragen selbst unter extremen Witterungsbedingungen und wechselnden Lastzyklen.
Ein hochwertiges **Kabelsystem für Windkraftanlagen** zeichnet sich durch mehrere Faktoren aus:
- **Materialqualität:** VPE-isolierte Kabel wie [**NA2XS(F)2Y** ](/de/products/medium-voltage-cables/na2xsf2y/)oder [**N2XS(F)2Y**](/de/products/medium-voltage-cables/n2xsf2y/) bieten hohe elektrische Festigkeit und exzellenten Langzeitschutz.
- **[Normkonformität](https://www.zvei.org/fileadmin/user_upload/Presse_und_Medien/Publikationen/2017/September/ZVEI_Leitfaden_Kabel_und_Leitungen_in_Windkraftanlagen/ZVEI-Leitfaden-Kabel-und-Leitungen-in-Windkraftanlagen-September-2017.pdf):** Alle eingesetzten Komponenten sollten den einschlägigen Normen wie **DIN VDE 0276**, **VDE 0298** oder **IEC 60502** entsprechen.
- **Normkonformität (PDF):** [Alle eingesetzten Komponenten sollten den einschlägigen Normen](https://www.zvei.org/fileadmin/user_upload/Presse_und_Medien/Publikationen/2017/September/ZVEI_Leitfaden_Kabel_und_Leitungen_in_Windkraftanlagen/ZVEI-Leitfaden-Kabel-und-Leitungen-in-Windkraftanlagen-September-2017.pdf) wie **DIN VDE 0276**, **VDE 0298** oder **IEC 60502** entsprechen.
- **Montagefreundlichkeit:** Die Kabelkonstruktion muss so ausgelegt sein, dass sie sich effizient und sicher verlegen lässt auch bei schwierigen Bodenbedingungen.
- **Umweltaspekte:** Recyclingfähige Materialien und die [Wiederverwendung von Trommeln oder Leitermaterialien](/de/recycling-von-kabeltrommeln-nachhaltigkeit-im-windkraftprojekt/) reduzieren den ökologischen Fußabdruck.
@@ -93,6 +95,8 @@ Immer mehr Projektentwickler legen Wert auf **nachhaltige Kabelsysteme**, die En
Die Kombination aus **technischer Qualität**, **ökologischer Verantwortung** und **effizienter Logistik** macht moderne **Windparkverkabelung** zu einem zentralen Erfolgsfaktor im Netzausbau. Wer hier auf durchdachte Lösungen setzt, schafft die Basis für einen stabilen und nachhaltigen Energiefluss heute und in Zukunft.
[Welche Kabel Sie für Ihr Windparkprojekt brauchen und welche Unterschiede es gibt, erklären wir Ihnen hier.](/de/welche-kabel-fuer-windkraft-unterschiede-von-nieder-bis-hoechstspannung-erklaert/)
<AnimatedImage src="/uploads/2025/01/offshore-wind-power-and-energy-farm-with-many-wind-2023-11-27-04-51-29-utc-scaled.webp" alt="Windpark Landschaft" width={2560} height={1707} />
## Fazit: Erfolgreich ans Netz
Die Verkabelung ist das Rückgrat jedes **Windparks** und gleichzeitig einer der sensibelsten Projektbereiche. Enge Zeitpläne, komplexe Logistik und spontane Änderungen sind dabei keine Ausnahme, sondern Alltag. Wer diese Herausforderungen frühzeitig erkennt und gezielt plant, verhindert Stillstand, Kostensteigerungen und Terminverschiebungen.
Erfolgreiche **Windpark-Kabelprojekte** zeichnen sich durch drei Dinge aus:
@@ -106,4 +110,4 @@ Ob [**Mittelspannung**](/de/stromkabel/mittelspannungskabel/), **Erdkabel** oder
Unser Vorteil liegt in der **Praxisnähe**: Wir wissen, wie eng Bauzeiten im Windparkbau sind, welche Kabelsysteme sich bewährt haben und worauf es bei der Logistik wirklich ankommt. Durch unsere **Lagerkapazitäten in der Mitte Deutschlands** reagieren wir schnell auf Änderungen und halten Lieferketten stabil auch wenn Projekte dynamisch verlaufen.
Mit unserem Netzwerk, unserer Marktkenntnis und unserer Leidenschaft für erneuerbare Energien sorgen wir dafür, dass Ihr **Windkraftprojekt** pünktlich und reibungslos ans Netz geht.
➡️ **Planen Sie ein neues Windparkprojekt oder benötigen Unterstützung bei der Kabelauswahl?**Dann sprechen Sie uns an wir liefern die **Kabel, Lösungen und Erfahrung**, die Ihr Projekt erfolgreich machen.
[Jetzt Kontakt aufnehmen](/de/contact/)
[Jetzt Kontakt aufnehmen](/de/kontakt/)

View File

@@ -1,6 +1,6 @@
---
title: 'Focus on wind farm construction: three typical cable challenges'
date: '2025-11-05T10:16:15'
date: '2025-01-05T10:16:15'
featuredImage: /uploads/2025/04/image_fx_-2025-02-20T193520.620.webp
locale: en
category: Kabel Technologie
@@ -28,21 +28,14 @@ In this article, we look at the** 3 biggest challenges in wind farm construction
Find out why onshore wind farms are a crucial pillar of the energy transition here:
<VisualLinkPreview
url="
<VisualLinkPreview
<VisualLinkPreview
url="https://www.enbw.com/unternehmen/themen/windkraft/onshore-wind-pfeiler-der-energiewende.html"
title="Onshore-Windenergie als Pfeiler der Energiewende | EnBW"
summary="Viele Faktoren haben den Bau von Windenergieanlagen in den letzten Jahren gebremst. Lesen Sie hier die Gründe!"
image="https://www.enbw.com/media/image-proxy/1600x914,q70,focus50x49,zoom1.0/https://www.enbw.com/media/presse/images/newsroom/onshore-windpark-langenburg-7zu4_1701415033580.jpg"
/>
"
title="Onshore-Windenergie als Pfeiler der Energiewende | EnBW"
summary="Viele Faktoren haben den Bau von Windenergieanlagen in den letzten Jahren gebremst. Lesen Sie hier die Gründe!"
image="https://www.enbw.com/media/image-proxy/1600x914,q70,focus50x49,zoom1.0/https://www.enbw.com/media/presse/images/newsroom/onshore-windpark-langenburg-7zu4_1701415033580.jpg"
/>
<AnimatedImage src="/uploads/2025/04/image_fx_-7.webp" alt="Wind farm construction site" width={1408} height={768} />
<AnimatedImage src="/uploads/2025/02/image_fx_-7.webp" alt="Wind farm construction site" width={1408} height={768} />
## Challenge 1: Tight construction timelines and fixed deadlines
@@ -70,19 +63,12 @@ With precise [**cable capacity**](https://www.a-eberle.de/infobrief/infobrief-20
Want to know which cable types are used in wind farms? Check out this article:
<VisualLinkPreview
url="
<VisualLinkPreview
<VisualLinkPreview
url="https://wind-turbine.com/magazin/ratgeber/250713/welche-arten-von-kabeln-benoetigt-man-fuer-den-bau-eines-windparks.html"
title="Welche Arten von Kabeln benötigt man für den Bau eines Windparks?"
summary="Die Verkabelung ist ein zentrales Element jeder Windkraftanlage und beeinflusst maßgeblich die Effizienz, Sicherheit und Wirtschaftlichkeit eines Windparks.…"
image="https://wind-turbine.com/i/53689/68738caa5e58ffdf06031cf2/2/1200/630/68738c85497af_KabelfreinenWindparkpng.png"
/>
"
title="Welche Arten von Kabeln benötigt man für den Bau eines Windparks?"
summary="Die Verkabelung ist ein zentrales Element jeder Windkraftanlage und beeinflusst maßgeblich die Effizienz, Sicherheit und Wirtschaftlichkeit eines Windparks.…"
image="https://wind-turbine.com/i/53689/68738caa5e58ffdf06031cf2/2/1200/630/68738c85497af_KabelfreinenWindparkpng.png"
/>
## Challenge 2: Large delivery volumes and specialized packaging
@@ -109,7 +95,7 @@ A clear [**cable logistics strategy**](https://logistik-heute.de/galerien/mammut
Anyone who integrates **packaging, storage and labeling** early into the planning process ensures that the **wind farm cables** arrive exactly where they&#8217;re needed with no time lost and no disruption to the construction flow.
<AnimatedImage src="/uploads/2025/08/NA2XSF2Y_3x1x300_RM-25_12-20kV-0.webp" alt="NA2XSF2Y Cable" width={1920} height={1080} />
<AnimatedImage src="/uploads/2025/08/NA2XSF2X_3x1x300_RM-25_12-20kV-3.webp" alt="NA2XSF2Y Cable" width={1920} height={1080} />
## Challenge 3: Last-minute project changes
@@ -132,19 +118,12 @@ Short-term changes arent the exception theyre part of everyday life in
Avoid delays or issues during your wind power project by understanding early on why NABU may file objections to certain sites:
<VisualLinkPreview
url="
<VisualLinkPreview
<VisualLinkPreview
url="https://www.nabu.de/umwelt-und-ressourcen/energie/erneuerbare-energien-energiewende/windenergie/26913.html"
title="Wann klagt der NABU gegen Windkraftprojekte?"
summary="45 Klagen wurden wegen Fehlplanungen bei Windenergie zwischen 2010 und 2019 vom NABU auf den Weg gebracht. Nicht weil der Windenergieausbau aufgehalten werden soll, sondern weil immer wieder Vorhaben und Planungen eklatant gegen Naturschutzrecht verstoßen."
image="https://www.nabu.de/imperia/md/nabu/images/umwelt/energie/energietraeger/windkraft/161125-nabu-windrad-allgaeu-heidrun-burchard.jpeg"
/>
"
title="Wann klagt der NABU gegen Windkraftprojekte?"
summary="45 Klagen wurden wegen Fehlplanungen bei Windenergie zwischen 2010 und 2019 vom NABU auf den Weg gebracht. Nicht weil der Windenergieausbau aufgehalten werden soll, sondern weil immer wieder Vorhaben und Planungen eklatant gegen Naturschutzrecht verstoßen."
image="https://www.nabu.de/imperia/md/nabu/images/umwelt/energie/energietraeger/windkraft/161125-nabu-windrad-allgaeu-heidrun-burchard.jpeg"
/>
## Quality and sustainability as success factors
@@ -152,7 +131,7 @@ In addition to time and logistics, [**cable quality**](https://www.windkraft-jou
A high-quality **cable system for wind power** stands out due to several factors:
- **Material quality:** XLPE-insulated cables like [**NA2XS(F)2Y**](/en/products/medium-voltage-cables/na2xsf2y/) or [**N2XS(F)2Y**](/en/products/medium-voltage-cables/n2xsf2y/) provide high dielectric strength and excellent long-term protection.
- **[Standards compliance](https://www.zvei.org/fileadmin/user_upload/Presse_und_Medien/Publikationen/2017/September/ZVEI_Leitfaden_Kabel_und_Leitungen_in_Windkraftanlagen/ZVEI-Leitfaden-Kabel-und-Leitungen-in-Windkraftanlagen-September-2017.pdf):** All components used should meet key standards such as **DIN VDE 0276**, **VDE 0298**, or **IEC 60502**.
- **Standards compliance (PDF):** [All components used should meet key standards](https://www.zvei.org/fileadmin/user_upload/Presse_und_Medien/Publikationen/2017/September/ZVEI_Leitfaden_Kabel_und_Leitungen_in_Windkraftanlagen/ZVEI-Leitfaden-Kabel-und-Leitungen-in-Windkraftanlagen-September-2017.pdf) such as **DIN VDE 0276**, **VDE 0298**, or **IEC 60502**.
- **Ease of installation:** Cable design must allow for efficient and safe installation even under difficult ground conditions.
- **Environmental aspects:** Recyclable materials and the [reuse of drums or conductor materials](/de/recycling-von-kabeltrommeln-nachhaltigkeit-im-windkraftprojekt/) help reduce ecological footprint.
@@ -162,7 +141,7 @@ The combination of **technical quality**, **ecological responsibility**, and **e
[Find out here which cables are suitable for your wind farm project and what makes the difference between low and high voltage options.](/de/welche-kabel-fuer-windkraft-unterschiede-von-nieder-bis-hoechstspannung-erklaert/)
<AnimatedImage src="/uploads/2025/01/green-alternative-eco-friendly-energy-windmill-tu-2023-11-27-05-10-51-utc-scaled.webp" alt="Wind farm landscape" width={2560} height={1707} />
<AnimatedImage src="/uploads/2025/01/offshore-wind-power-and-energy-farm-with-many-wind-2023-11-27-04-51-29-utc-scaled.webp" alt="Wind farm landscape" width={2560} height={1707} />
## Conclusion: Successfully connected to the grid

View File

@@ -1,12 +1,11 @@
---
title: Terms Deutsch
excerpt: Liefer- und Zahlungsbedingungen der KLZ Vertriebs GmbH
title: AGB
excerpt: Liefer- und Zahlungsbedingungen der KLZ Vertriebs GmbH.
featuredImage: null
locale: de
---
# Liefer- und Zahlungsbedingungen
Stand November 2024
*Stand Januar 2026*
## 1. Allgemeines
@@ -22,13 +21,12 @@ Sofern nicht ausdrücklich als bindend bezeichnet, sind unsere Angebote freiblei
## 3. Preise
Alle von uns genannten Preise verstehen sich zzgl. der jeweiligen gesetzlichen Mehrwertsteuer vor Metallzuschlag fracht- frei innerhalb der Bundesrepublik Deutschland (Festland), jedoch ohne Abladen. Die Verkaufspreise, soweit sie als Hohlpreis deklariert sind, enthalten keinerlei Metallwerte. Diese werden zusätzlich separat berechnet.
Die Preise gelten für den in unseren Angeboten und Auftragsbestätigungen aufgeführten Leistungs- und Lieferumfang. Mehrleistungen werden gesondert berechnet. Die Hohlpreise verstehen sich in Euro zuzüglich Metallzuschlag, gegebenenfalls Verpackung, auftragsspezifischer Schnittkosten und der gesetzlichen Mehrwertsteuer.
## 4. Metallnotierung
Basis zur Kupferabrechnung ist die Notierung "LME Copper official price cash offer", Durchschnitt des Liefervormonats zuzüglich der dann aktuellen von uns benannten Kupfer-Prämie.
Basis zur Aluminiumabrechnung ist die Notierung "LME Aluminium official price cash offer", Durchschnitt des Liefervormonats zuzüglich der dann von uns benannten Aluminium-Prämie. USD werden auf Basis des EUR/USD LME-FX-Rate (MTLE) in EUR umgerechnet. Die entsprechenden Notierungen können Sie der Web-Seite www.westmetall.com entnehmen. Die Prämienzuschläge können stark variieren und KLZ behält sich das Recht vor, diese fristgerecht anzupassen, ungeachtet der Angebotslegung.
Basis zur Kupferabrechnung ist die Notierung LME Copper official price cash offer, Durchschnitt des Liefervormonats zuzüglich der dann aktuellen von uns benannten Kupfer-Prämie.
Basis zur Aluminiumabrechnung ist die Notierung „LME Aluminium official price cash offer“, Durchschnitt des Liefervormonats zuzüglich der dann von uns benannten Aluminium-Prämie. USD werden auf Basis des EUR/USD LME-FX-Rate (MTLE) in EUR umgerechnet. Die entsprechenden Notierungen können Sie der Web-Seite [www.westmetall.com](https://www.westmetall.com) entnehmen. Die Prämienzuschläge können stark variieren und KLZ behält sich das Recht vor, diese fristgerecht anzupassen, ungeachtet der Angebotslegung.
## 5. Metallzahl
@@ -44,7 +42,7 @@ Wir behalten uns an den von uns gelieferten Waren nachfolgend: Vorbehaltswar
## 8. Zahlungsbedingungen | Aufrechnung | Zurückbehaltungsrechte
Unsere Rechnungen sind 14 Tage nach Rechnungsdatum ohne jeden Abzug zahlbar. Bei Nichteinhaltung der vereinbarten Zahlungsbedingungen sind wir berechtigt, Zinsen in Höhe von 7 %-Punkten über dem Basiszinssatz zu berechnen; das Recht zur Geltendmachung weitergehender Schäden, insbesondere nachgewiesener höherer Zinsen, bleibt hiervon unberührt.
Unsere Rechnungen sind 10 Tage nach Rechnungsdatum ohne jeden Abzug zahlbar. Rechnungsstellung bzw. Datum ist grundsätzlich der Tag der Übergabe an den Spediteur soweit wir aus unseren deutschen Lägern liefern. Ansonsten gilt bei Direktimporten der Tag der Verzollung, der zeitnah zum Anliefertag liegt. Bei Nichteinhaltung der vereinbarten Zahlungsbedingungen sind wir berechtigt, Zinsen in Höhe von 7 %-Punkten über dem Basiszinssatz zu berechnen; das Recht zur Geltendmachung weitergehender Schäden, insbesondere nachgewiesener höherer Zinsen, bleibt hiervon unberührt.
## 9. Liefervorbehalt | Teillieferungen
@@ -58,7 +56,7 @@ Der Eintritt unseres Lieferverzugs bestimmt sich nach den gesetzlichen Vorschrif
Die gesetzlichen Rechte bleiben im Übrigen unberührt.
Fixgeschäfte setzen die ausdrückliche schriftliche Bezeichnung als solche voraus. Ansonsten ist der Besteller stets verpflichtet, uns schriftlich eine angemessene Nachfrist zu setzen, wenn von uns zugesagte Termine und/ oder Fristen nicht eingehalten werden. Wird auch die Nachfrist nicht eingehalten, ist der Besteller berechtigt, vom Vertrag zurückzutreten.
Fixgeschäfte setzen die ausdrückliche schriftliche Bezeichnung als solche voraus. Ansonsten ist der Besteller stets verpflichtet, uns schriftlich eine angemessene Nachfrist zu setzen, wenn von uns zugesagte Termine und/ oder Fristen nicht einhalten werden. Wird auch die Nachfrist nicht eingehalten, ist der Besteller berechtigt, vom Vertrag zurückzutreten.
Im Fall höherer Gewalt und/oder sonstiger von uns nicht vorhersehbarer außergewöhnlicher und/oder unverschuldeter Umstände, auch wenn sie bei unserem Vorlieferanten eintreten, verlängert sich eine von uns zugesagte Lieferfrist bis zur Behebung des vorerwähnten Ereignisses. Ist dieser Zeitpunkt nicht überblickbar, sind sowohl der Besteller als auch wir berechtigt, von dem abgeschlossenen Vertrag zurückzutreten. In diesem Fall sind beiderseits Schadensersatzansprüche ausgeschlossen. Wir verpflichten uns, bei Bekanntwerden vorerwähnter Umstände den Besteller hiervon unverzüglich zu benachrichtigen.
@@ -70,13 +68,13 @@ Wird uns ein Abrufauftrag erteilt und werden über die Abruftermine keine gesond
## 12. Maß- und Gewichtsangaben
Alle Angaben über Durchmesser, Gewicht, technische Gestaltung, Herstellung und Umfang der von uns zu liefernden Ware stehen unter dem Vorbehalt der Abweichung innerhalb der handelsüblichen zulässigen Toleranzen. Darüber hinaus behalten wir uns Änderungen, die einer technischen Verbesserung dienen, jederzeit vor. Farbabweichungen und/oder Abweichungen in der äußeren Beschaffenheit der von uns zu liefernden Ware, die jedoch deren Qualität und technische Wirksamkeit unbeeinflusst lassen, begründen keine Mängelhaftungsansprüche des Bestellers.
Alle Angaben über Durchmesser, Gewicht, technische Gestaltung, Herstellung und Umfang der von uns zu liefernden Ware stehen unter dem Vorbehalt der Abweichung innerhalb der handelsüblichen zulässigen Toleranzen. Darüber hinaus behalten wir uns Änderungen, die einer technischen Verbesserung dienen, jederzeit vor. Farbabweichungen und/oder Abweichungen in der äußeren Beschaffenheit der von uns zu liefernden Ware, die jedoch deren Qualität und technische Wirksamkeit unbeeinflusst lässt, begründen keine Mängelhaftungsansprüche des Bestellers.
## 13. Gefahrübergang und -tragung
Die Lieferung erfolgt DAP frei Bestimmungsort Deutschland, wo auch der Erfüllungsort für die Lieferung und eine etwaige Nacherfüllung ist.
Wird die bestellte Ware von uns versandbereit gestellt und/oder verzögert sich die Versendung und/oder der Abruf aus Gründen, die vom Besteller zu vertreten sind, sind wir berechtigt, Ersatz des hieraus entstehenden Schadens einschließlich Mehraufwendungen zu verlangen. Hierfür berechnen wir eine pauschale Entschädigung i.H.v 2% des Rechnungsbetrages für jeden angefangenen Monat, maximal jedoch 10 % insgesamt beginnend mit der Lieferfrist bzw. mangels einer Lieferfrist mit der Mitteilung der Versandbereitschaft der Ware.
Wird die bestellte Ware von uns versandbereit gestellt und/oder verzögert sich die Versendung und/oder der Abruf aus Gründen, die vom Besteller zu vertreten sind, sind wir berechtigt, Ersatz des hieraus entstehenden Schadens einschließlich Mehraufwendungen für Einlagerungen zu verlangen. Hierfür berechnen wir eine pauschale Entschädigung i.H. von 2% des Rechnungsbetrages für jeden angefangenen Monat, maximal jedoch 10 % insgesamt beginnend mit der Lieferfrist bzw. mangels einer Lieferfrist mit der Mitteilung der Versandbereitschaft der Ware.
Der Nachweis eines höheren Schadens und unsere gesetzlichen Ansprüche (insbesondere Ersatz von Mehraufwendungen, angemessene Entschädigung, Kündigung) bleiben unberührt; die Pauschale ist aber auf weitergehende Geldansprüche anzurechnen. Dem Besteller bleibt der Nachweis gestattet, dass uns überhaupt kein oder nur ein wesentlich geringerer Schaden als vorstehende Pauschale entstanden ist. Rücksendungen an uns, die nicht vorher von uns schriftlich bestätigt worden sind, erfolgen auf alleinige Gefahr des Bestellers.
@@ -94,7 +92,9 @@ Weitergehende Ansprüche des Bestellers, gleich aus welchem Rechtsgrund, sind na
Die Verjährungsfristen für Mängelhaftungsansprüche beträgt 24 Monate ab Übergabe der Ware.
Sollte es bei einer Mängelrüge zu unterschiedlichen Meinungen bezüglich des Kabelschaden kommen, gilt hier im Zweifelsfall nur die Expertise des VDE-Instituts selbst. Andere, auch akkreditierte Testlabore, akzeptieren wir nicht. Wir weisen ausdrücklich daraufhin, dass beim Verlegen des Kabels in den Graben oder in Rohren, bzw. in Bauwerke eine ständige Sichtkontrolle durch den Kabelverleger vorzunehmen ist, ob Auffälligkeiten zu vermerken sind. Eine spätere Reklamation, die fahrlässiges Verhalten vermuten lässt, schränkt sich damit ein.
Sollte es bei einer Mängelrüge zu unterschiedlichen Meinungen bezüglich des Kabelschaden kommen, gilt hier im Zweifelsfall nur die Expertise des VDE-Instituts selbst. Andere, auch akkreditierte Testlabore, akzeptieren wir nicht.
Wir weisen ausdrücklich daraufhin, dass beim Verlegen des Kabels in den Gräben oder in Rohren, bzw. in Bauwerke eine ständige Sichtkontrolle durch den Kabelverleger vorzunehmen ist, ob Auffälligkeiten zu vermerken sind. Eine spätere Reklamation, die fahrlässiges Verhalten vermuten lässt, schränkt sich damit ein. Dies gilt auch bei der Annahme der Ware, wo offensichtliche Beschädigungen direkt zu kommunizieren sind. Spätere Ansprüche nach Akzeptanz einer einwandfreien Belieferung sind detailliert zu beweisen.
## 15. Schadenersatz | Gesamthaftung
@@ -110,4 +110,6 @@ Es gilt ausschließlich das Recht der Bundesrepublik Deutschland unter Ausschlus
Mit der Veröffentlichung der vorliegenden L&Z im Internet werden alle von uns früher verwendeten Bedingungen gegenstandslos.
Remshalden, 28.1.2026
[Download als PDF](/AGB-KLZ-1-2026.pdf)

View File

@@ -1,53 +1,59 @@
---
title: Privacy Policy &#8211; Deutsch
excerpt: >-
[vc_column column_padding=&#8221;no-extra-padding&#8221;
column_padding_tablet=&#8221;inherit&#8221;
column_padding_phone=&#8221;inherit&#8221;
column_padding_position=&#8221;all&#8221;
column_element_direction_desktop=&#8221;default&#8221;
column_element_spacing=&#8221;default&#8221;
desktop_text_alignment=&#8221;default&#8221;
tablet_text_alignment=&#8221;default&#8221;
phone_text_alignment=&#8221;default&#8221;
background_color_opacity=&#8221;1&#8243;
background_hover_color_opacity=&#8221;1&#8243;
column_backdrop_filter=&#8221;none&#8221;
column_shadow=&#8221;none&#8221;&#8230;
title: Datenschutzerklärung
excerpt: Informationen zum Umgang mit Ihren persönlichen Daten bei der KLZ Vertriebs GmbH.
featuredImage: null
locale: de
---
# Privacy Policy &#8211; Deutsch
<h1>Datenschutzerklärung</h1>
<h3>1. Datenschutz auf einen Blick</h3>
<p><strong>Allgemeine Hinweise</strong><br />
Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten passiert, wenn Sie unsere Website besuchen. Personenbezogene Daten sind alle Daten, mit denen Sie persönlich identifiziert werden können. Ausführliche Informationen zum Thema Datenschutz entnehmen Sie unserer unten aufgeführten Datenschutzerklärung.</p>
<hr />
<h3>2. Allgemeine Hinweise und Pflichtinformationen</h3>
<p><strong>Datenschutz</strong><br />
Die Betreiber dieser Seiten nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend der gesetzlichen Datenschutzvorschriften sowie dieser Datenschutzerklärung.</p>
<p>Wenn Sie diese Website nutzen, werden verschiedene personenbezogene Daten erhoben. Personenbezogene Daten sind Daten, mit denen Sie persönlich identifiziert werden können. Diese Datenschutzerklärung erläutert, welche Daten wir erheben und wofür wir sie nutzen. Sie erläutert auch, wie und zu welchem Zweck dies geschieht.</p>
<p>Wir weisen darauf hin, dass die Datenübertragung im Internet (z. B. bei der Kommunikation per E-Mail) Sicherheitslücken aufweisen kann. Ein lückenloser Schutz der Daten vor dem Zugriff durch Dritte ist nicht möglich.</p>
<hr />
<p><strong>Widerruf Ihrer Einwilligung zur Datenverarbeitung</strong><br />
Viele Datenverarbeitungsvorgänge sind nur mit Ihrer ausdrücklichen Einwilligung möglich. Sie können eine bereits erteilte Einwilligung jederzeit widerrufen. Dazu reicht eine formlose Mitteilung per E-Mail an uns. Die Rechtmäßigkeit der bis zum Widerruf erfolgten Datenverarbeitung bleibt vom Widerruf unberührt.</p>
<hr />
<p><strong>Beschwerderecht bei der zuständigen Aufsichtsbehörde</strong><br />
Im Falle von Verstößen gegen das Datenschutzrecht steht dem Betroffenen ein Beschwerderecht bei der zuständigen Aufsichtsbehörde zu. Die zuständige Aufsichtsbehörde für Datenschutzfragen ist der Datenschutzbeauftragte des Bundeslandes, in dem unser Unternehmen seinen Sitz hat. Eine Liste der Datenschutzbeauftragten sowie deren Kontaktdaten können Sie folgendem Link entnehmen:<br />
<a href="https://www.bfdi.bund.de/DE/Infothek/Anschriften_Links/anschriften_links-node.html" target="_new" rel="noopener noreferrer nofollow" target="_blank">https://www.bfdi.bund.de/DE/Infothek/Anschriften_Links/anschriften_links-node.html</a>.</p>
<hr />
<p><strong>Recht auf Datenübertragbarkeit</strong><br />
Sie haben das Recht, Daten, die wir auf Grundlage Ihrer Einwilligung oder zur Erfüllung eines Vertrags automatisiert verarbeiten, an sich oder an einen Dritten in einem gängigen, maschinenlesbaren Format aushändigen zu lassen. Sofern Sie die direkte Übertragung der Daten an einen anderen Verantwortlichen verlangen, erfolgt dies nur, soweit es technisch machbar ist.</p>
<hr />
<p><strong>Auskunft, Sperrung, Löschung</strong><br />
Im Rahmen der geltenden gesetzlichen Bestimmungen haben Sie jederzeit das Recht auf unentgeltliche Auskunft über Ihre gespeicherten personenbezogenen Daten, deren Herkunft und Empfänger sowie den Zweck der Datenverarbeitung. Gegebenenfalls haben Sie ein Recht auf Berichtigung, Sperrung oder Löschung dieser Daten. Hierzu sowie zu weiteren Fragen zum Thema personenbezogene Daten können Sie sich jederzeit unter der im Impressum angegebenen Adresse an uns wenden.</p>
<hr />
<p><strong>Widerspruch gegen Werbe-E-Mails</strong><br />
Der Nutzung von im Rahmen der Impressumspflicht veröffentlichten Kontaktdaten zur Übersendung von nicht ausdrücklich angeforderter Werbung und Informationsmaterialien wird hiermit widersprochen. Die Betreiber der Seiten behalten sich ausdrücklich rechtliche Schritte im Falle der unverlangten Zusendung von Werbeinformationen, etwa durch Spam-E-Mails, vor.</p>
<hr />
<h3>3. Datenerfassung in unserem Unternehmen</h3>
<p><strong>Datenübermittlung bei Vertragsschluss für Dienstleistungen und digitale Inhalte</strong><br />
Wir übermitteln personenbezogene Daten an Dritte nur dann, wenn dies im Rahmen der Vertragsabwicklung notwendig ist, z. B. an das mit der Zahlungsabwicklung beauftragte Kreditinstitut.</p>
<p>Eine weitergehende Übermittlung der Daten erfolgt nicht bzw. nur dann, wenn Sie der Übermittlung ausdrücklich zugestimmt haben. Eine Weitergabe Ihrer Daten an Dritte ohne ausdrückliche Einwilligung, etwa zu Werbezwecken, erfolgt nicht.</p>
<p>Rechtsgrundlage für die Datenverarbeitung ist Art. 6 Abs. 1 lit. b DSGVO, der die Verarbeitung von Daten zur Erfüllung eines Vertrags oder vorvertraglicher Maßnahmen gestattet.
## 1. Datenschutz auf einen Blick
### Allgemeine Hinweise
Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten passiert, wenn Sie unsere Website besuchen. Personenbezogene Daten sind alle Daten, mit denen Sie persönlich identifiziert werden können. Ausführliche Informationen zum Thema Datenschutz entnehmen Sie unserer unten aufgeführten Datenschutzerklärung.
---
## 2. Allgemeine Hinweise und Pflichtinformationen
### Datenschutz
Die Betreiber dieser Seiten nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend der gesetzlichen Datenschutzvorschriften sowie dieser Datenschutzerklärung.
Wenn Sie diese Website nutzen, werden verschiedene personenbezogene Daten erhoben. Personenbezogene Daten sind Daten, mit denen Sie persönlich identifiziert werden können. Diese Datenschutzerklärung erläutert, welche Daten wir erheben und wofür wir sie nutzen. Sie erläutert auch, wie und zu welchem Zweck dies geschieht.
Wir weisen darauf hin, dass die Datenübertragung im Internet (z. B. bei der Kommunikation per E-Mail) Sicherheitslücken aufweisen kann. Ein lückenloser Schutz der Daten vor dem Zugriff durch Dritte ist nicht möglich.
---
### Widerruf Ihrer Einwilligung zur Datenverarbeitung
Viele Datenverarbeitungsvorgänge sind nur mit Ihrer ausdrücklichen Einwilligung möglich. Sie können eine bereits erteilte Einwilligung jederzeit widerrufen. Dazu reicht eine formlose Mitteilung per E-Mail an uns. Die Rechtmäßigkeit der bis zum Widerruf erfolgten Datenverarbeitung bleibt vom Widerruf unberührt.
---
### Beschwerderecht bei der zuständigen Aufsichtsbehörde
Im Falle von Verstößen gegen das Datenschutzrecht steht dem Betroffenen ein Beschwerderecht bei der zuständigen Aufsichtsbehörde zu. Die zuständige Aufsichtsbehörde für Datenschutzfragen ist der Datenschutzbeauftragte des Bundeslandes, in dem unser Unternehmen seinen Sitz hat. Eine Liste der Datenschutzbeauftragten sowie deren Kontaktdaten können Sie folgendem Link entnehmen:
[https://www.bfdi.bund.de/DE/Infothek/Anschriften_Links/anschriften_links-node.html](https://www.bfdi.bund.de/DE/Infothek/Anschriften_Links/anschriften_links-node.html).
---
### Recht auf Datenübertragbarkeit
Sie haben das Recht, Daten, die wir auf Grundlage Ihrer Einwilligung oder zur Erfüllung eines Vertrags automatisiert verarbeiten, an sich oder an einen Dritten in einem gängigen, maschinenlesbaren Format aushändigen zu lassen. Sofern Sie die direkte Übertragung der Daten an einen anderen Verantwortlichen verlangen, erfolgt dies nur, soweit es technisch machbar ist.
---
### Auskunft, Sperrung, Löschung
Im Rahmen der geltenden gesetzlichen Bestimmungen haben Sie jederzeit das Recht auf unentgeltliche Auskunft über Ihre gespeicherten personenbezogenen Daten, deren Herkunft und Empfänger sowie den Zweck der Datenverarbeitung. Gegebenenfalls haben Sie ein Recht auf Berichtigung, Sperrung oder Löschung dieser Daten. Hierzu sowie zu weiteren Fragen zum Thema personenbezogene Daten können Sie sich jederzeit unter der im Impressum angegebenen Adresse an uns wenden.
---
### Widerspruch gegen Werbe-E-Mails
Der Nutzung von im Rahmen der Impressumspflicht veröffentlichten Kontaktdaten zur Übersendung von nicht ausdrücklich angeforderter Werbung und Informationsmaterialien wird hiermit widersprochen. Die Betreiber der Seiten behalten sich ausdrücklich rechtliche Schritte im Falle der unverlangten Zusendung von Werbeinformationen, etwa durch Spam-E-Mails, vor.
---
## 3. Datenerfassung in unserem Unternehmen
### Datenübermittlung bei Vertragsschluss für Dienstleistungen und digitale Inhalte
Wir übermitteln personenbezogene Daten an Dritte nur dann, wenn dies im Rahmen der Vertragsabwicklung notwendig ist, z. B. an das mit der Zahlungsabwicklung beauftragte Kreditinstitut.
Eine weitergehende Übermittlung der Daten erfolgt nicht bzw. nur dann, wenn Sie der Übermittlung ausdrücklich zugestimmt haben. Eine Weitergabe Ihrer Daten an Dritte ohne ausdrückliche Einwilligung, etwa zu Werbezwecken, erfolgt nicht.
Rechtsgrundlage für die Datenverarbeitung ist Art. 6 Abs. 1 lit. b DSGVO, der die Verarbeitung von Daten zur Erfüllung eines Vertrags oder vorvertraglicher Maßnahmen gestattet.

View File

@@ -1,34 +1,26 @@
---
title: Legal Notice &#8211; Deutsch
excerpt: >-
[vc_column column_padding=&#8221;no-extra-padding&#8221;
column_padding_tablet=&#8221;inherit&#8221;
column_padding_phone=&#8221;inherit&#8221;
column_padding_position=&#8221;all&#8221;
column_element_direction_desktop=&#8221;default&#8221;
column_element_spacing=&#8221;default&#8221;
desktop_text_alignment=&#8221;default&#8221;
tablet_text_alignment=&#8221;default&#8221;
phone_text_alignment=&#8221;default&#8221;
background_color_opacity=&#8221;1&#8243;
background_hover_color_opacity=&#8221;1&#8243;
column_backdrop_filter=&#8221;none&#8221;
column_shadow=&#8221;none&#8221;&#8230;
title: Impressum
excerpt: Rechtliche Informationen und Kontaktdaten der KLZ Vertriebs GmbH.
featuredImage: null
locale: de
---
# Legal Notice &#8211; Deutsch
<h1>Impressum</h1>
<p><strong>Verantwortlich für den Inhalt:</strong><br />
Michael Bodemer</p>
<p><strong>KLZ Vertriebs GmbH</strong><br />
Raiffeisenstraße 22<br />
73630 Remshalden</p>
<p><a rel="noopener">info@klz-cables.com</a><br />
<a href="http://www.klz-cables.com" target="_new" rel="noopener noreferrer nofollow" target="_blank">www.klz-cables.com</a></p>
<p>Amtsgericht Stuttgart<br />
HRB-Nr. 798037<br />
Gerichtsstand: Stuttgart</p>
<p><strong>Urheberrecht:</strong><br />
## Verantwortlich für den Inhalt
Michael Bodemer
**KLZ Vertriebs GmbH**
Raiffeisenstraße 22
73630 Remshalden
Deutschland
## Kontakt
E-Mail: [info@klz-cables.com](mailto:info@klz-cables.com)
Web: [www.klz-cables.com](https://www.klz-cables.com)
## Registereintrag
Amtsgericht Stuttgart
HRB-Nr. 798037
Gerichtsstand: Stuttgart
## Urheberrecht
Alle auf dieser Website veröffentlichten Texte, Bilder und sonstigen Informationen unterliegen dem Urheberrecht, sofern nicht anders angegeben. Jegliche Vervielfältigung, Verbreitung, Speicherung, Übermittlung, Nachbildung oder Weitergabe der Inhalte ist ohne vorherige schriftliche Genehmigung ausdrücklich untersagt. Für weitere Informationen wenden Sie sich bitte an die oben genannte Adresse.

View File

@@ -1,34 +1,26 @@
---
title: Legal Notice &#8211; English
excerpt: >-
[vc_column column_padding=&#8221;no-extra-padding&#8221;
column_padding_tablet=&#8221;inherit&#8221;
column_padding_phone=&#8221;inherit&#8221;
column_padding_position=&#8221;all&#8221;
column_element_direction_desktop=&#8221;default&#8221;
column_element_spacing=&#8221;default&#8221;
desktop_text_alignment=&#8221;default&#8221;
tablet_text_alignment=&#8221;default&#8221;
phone_text_alignment=&#8221;default&#8221;
background_color_opacity=&#8221;1&#8243;
background_hover_color_opacity=&#8221;1&#8243;
column_backdrop_filter=&#8221;none&#8221;
column_shadow=&#8221;none&#8221;&#8230;
title: Legal Notice
excerpt: Legal information and contact details for KLZ Vertriebs GmbH.
featuredImage: null
locale: en
---
# Legal Notice &#8211; English
<h1>Legal Notice</h1>
<p><strong>Responsible for the content:</strong><br />
Michael Bodemer</p>
<p>KLZ Vertriebs GmbH<br />
Raiffeisenstraße 22<br />
73630 Remshalden</p>
<p>info@klz-cables.com<br />
www.klz-cables.com</p>
<p>Local Court Stuttgart<br />
HRB-Nr. 798037<br />
Place of jurisdiction: Stuttgart</p>
<p>Copyright:<br />
## Responsible for the content
Michael Bodemer
**KLZ Vertriebs GmbH**
Raiffeisenstraße 22
73630 Remshalden
Germany
## Contact
Email: [info@klz-cables.com](mailto:info@klz-cables.com)
Web: [www.klz-cables.com](https://www.klz-cables.com)
## Registration
Local Court Stuttgart
HRB-Nr. 798037
Place of jurisdiction: Stuttgart
## Copyright
All texts, images and other information published on the website are subject to copyright unless otherwise indicated. Any duplication, distribution, storage, transmission, reproduction or forwarding of the contents without written permission is expressly prohibited. For further information, please contact the above address.

View File

@@ -1,41 +1,36 @@
---
title: Privacy Policy &#8211; English
excerpt: >-
[vc_column column_padding=&#8221;no-extra-padding&#8221;
column_padding_tablet=&#8221;inherit&#8221;
column_padding_phone=&#8221;inherit&#8221;
column_padding_position=&#8221;all&#8221;
column_element_direction_desktop=&#8221;default&#8221;
column_element_spacing=&#8221;default&#8221;
desktop_text_alignment=&#8221;default&#8221;
tablet_text_alignment=&#8221;default&#8221;
phone_text_alignment=&#8221;default&#8221;
background_color_opacity=&#8221;1&#8243;
background_hover_color_opacity=&#8221;1&#8243;
column_backdrop_filter=&#8221;none&#8221;
column_shadow=&#8221;none&#8221;&#8230;
title: Privacy Policy
excerpt: Information on how we handle your personal data at KLZ Vertriebs GmbH.
featuredImage: null
locale: en
---
# Privacy Policy &#8211; English
<h1>Privacy Policy</h1>
<h2 class="text-2xl mb-4">1. Data protection at a glance</h2>
<h3 class="text-xl">General information</h3>
<p class="mb-4">The following information provides a simple overview of what happens to your personal data when you visit our website. Personal data are all data with which you can be personally identified. For detailed information on the subject of data protection, please refer to our data protection declaration listed below this text.</p>
<h2 class="text-2xl mb-4">2. General notes and compulsory information</h2>
<h3 class="text-xl">Data protection</h3>
<p class="mb-4">The operators of these pages take the protection of your personal data very seriously. We treat your personal data confidentially and in accordance with the legal data protection regulations and this data protection declaration. When you use this website, various personal data is collected. Personal data is data with which you can be personally identified. This privacy policy explains what data we collect and what we use it for. It also explains how we do this and for what purpose. We would like to point out that data transmission over the Internet (e.g. communication by e-mail) can have security gaps. It is not possible to completely protect data from access by third parties.</p>
<h3 class="text-xl">Revocation of your consent to data processing</h3>
<p class="mb-4">Many data processing operations are only possible with your express consent. You can revoke a previously granted consent at any time. For this purpose, an informal notification by e-mail to us is sufficient. The lawfulness of the data processing that took place up to the revocation remains unaffected by the revocation.</p>
<h3 class="text-xl">Right of appeal to the competent supervisory authority</h3>
<p class="mb-4">In the event of violations of data protection law, the person concerned has a right of appeal to the competent supervisory authority. The competent supervisory authority for data protection issues is the data protection commissioner of the federal state in which our company is located. A list of the data protection officers and their contact details can be found at the following link: https://www.bfdi.bund.de/DE/Infothek/Anschriften_Links/anschriften_links-node.html.</p>
<h3 class="text-xl">Right to data transferability</h3>
<p class="mb-4">You have the right to have data which we process automatically on the basis of your consent or in fulfilment of a contract handed over to you or to a third party in a common, machine-readable format. If you request the direct transfer of the data to another responsible party, this will only be done to the extent that it is technically feasible.</p>
<h3 class="text-xl">Information, blocking, deletion</h3>
<p class="mb-4">Within the framework of the applicable legal provisions, you have the right at any time to receive information free of charge about your stored personal data, its origin and recipients and the purpose of the data processing and, if applicable, a right to correct, block or delete this data. For this purpose, as well as for further questions regarding personal data, you can contact us at any time at the address given in the imprint.</p>
<h3 class="text-xl">Contradiction against advertising mails</h3>
<p class="mb-4">The use of contact data published within the scope of the imprint obligation for the transmission of not expressly requested advertising and information material is hereby contradicted. The operators of the site expressly reserve the right to take legal action in the event of unsolicited sending of advertising information, such as through spam e-mails.</p>
<h2 class="text-2xl mb-4">3. Data collection in our company</h2>
<h3 class="text-xl">Data transfer upon conclusion of the contract for services and digital contents</h3>
<p class="mb-4">We only transfer personal data to third parties if this is necessary within the scope of processing the contract, e.g. to the credit institution commissioned with processing payments. Any further transmission of data will not take place or only if you have expressly agreed to the transmission. Your data will not be passed on to third parties without your express consent, for example for advertising purposes. The basis for data processing is Art. 6 Par. 1 letter b DSGVO, which permits the processing of data for the fulfilment of a contract or pre-contractual measures.</p>
## 1. Data protection at a glance
### General information
The following information provides a simple overview of what happens to your personal data when you visit our website. Personal data are all data with which you can be personally identified. For detailed information on the subject of data protection, please refer to our data protection declaration listed below this text.
## 2. General notes and compulsory information
### Data protection
The operators of these pages take the protection of your personal data very seriously. We treat your personal data confidentially and in accordance with the legal data protection regulations and this data protection declaration. When you use this website, various personal data is collected. Personal data is data with which you can be personally identified. This privacy policy explains what data we collect and what we use it for. It also explains how we do this and for what purpose. We would like to point out that data transmission over the Internet (e.g. communication by e-mail) can have security gaps. It is not possible to completely protect data from access by third parties.
### Revocation of your consent to data processing
Many data processing operations are only possible with your express consent. You can revoke a previously granted consent at any time. For this purpose, an informal notification by e-mail to us is sufficient. The lawfulness of the data processing that took place up to the revocation remains unaffected by the revocation.
### Right of appeal to the competent supervisory authority
In the event of violations of data protection law, the person concerned has a right of appeal to the competent supervisory authority. The competent supervisory authority for data protection issues is the data protection commissioner of the federal state in which our company is located. A list of the data protection officers and their contact details can be found at the following link: [https://www.bfdi.bund.de/DE/Infothek/Anschriften_Links/anschriften_links-node.html](https://www.bfdi.bund.de/DE/Infothek/Anschriften_Links/anschriften_links-node.html).
### Right to data transferability
You have the right to have data which we process automatically on the basis of your consent or in fulfilment of a contract handed over to you or to a third party in a common, machine-readable format. If you request the direct transfer of the data to another responsible party, this will only be done to the extent that it is technically feasible.
### Information, blocking, deletion
Within the framework of the applicable legal provisions, you have the right at any time to receive information free of charge about your stored personal data, its origin and recipients and the purpose of the data processing and, if applicable, a right to correct, block or delete this data. For this purpose, as well as for further questions regarding personal data, you can contact us at any time at the address given in the imprint.
### Contradiction against advertising mails
The use of contact data published within the scope of the imprint obligation for the transmission of not expressly requested advertising and information material is hereby contradicted. The operators of the site expressly reserve the right to take legal action in the event of unsolicited sending of advertising information, such as through spam e-mails.
## 3. Data collection in our company
### Data transfer upon conclusion of the contract for services and digital contents
We only transfer personal data to third parties if this is necessary within the scope of processing the contract, e.g. to the credit institution commissioned with processing payments. Any further transmission of data will not take place or only if you have expressly agreed to the transmission. Your data will not be passed on to third parties without your express consent, for example for advertising purposes. The basis for data processing is Art. 6 Par. 1 letter b DSGVO, which permits the processing of data for the fulfilment of a contract or pre-contractual measures.

View File

@@ -1,12 +1,11 @@
---
title: Terms English
excerpt: Delivery and Payment Terms of KLZ Vertriebs GmbH
title: Terms
excerpt: Delivery and Payment Terms of KLZ Vertriebs GmbH.
featuredImage: null
locale: en
---
# Delivery and Payment Terms
Status November 2024
*Status January 2026*
## 1. General
@@ -22,13 +21,13 @@ Unless expressly designated as binding, our offers are non-binding; the customer
## 3. Prices
All prices stated by us are understood plus the respective statutory value-added tax, before metal surcharge, freight-free within the Federal Republic of Germany (mainland), however without unloading. The sales prices, insofar as they are declared as hollow prices, contain no metal values whatsoever. These are additionally calculated separately.
The prices apply to the scope of services and deliveries listed in our offers and order confirmations. Additional services will be charged separately. The hollow prices are in Euro plus metal surcharge, if applicable packaging, order-specific cutting costs and the statutory value-added tax.
## 4. Metal quotation
Basis for copper billing is the quotation "LME Copper official price cash offer", average of the delivery month plus the then current copper premium named by us.
Basis for aluminum billing is the quotation "LME Aluminium official price cash offer", average of the delivery month plus the then current aluminum premium named by us. USD are converted to EUR on the basis of the EUR/USD LME-FX-Rate (MTLE). The corresponding quotations can be taken from the website www.westmetall.com. The premium surcharges can vary strongly and KLZ reserves the right to adjust these in due time, regardless of the offer submission.
Basis for aluminum billing is the quotation "LME Aluminium official price cash offer", average of the delivery month plus the then current aluminum premium named by us. USD are converted to EUR on the basis of the EUR/USD LME-FX-Rate (MTLE). The corresponding quotations can be taken from the website [www.westmetall.com](https://www.westmetall.com). The premium surcharges can vary strongly and KLZ reserves the right to adjust these in due time, regardless of the offer submission.
## 5. Metal number
@@ -44,7 +43,7 @@ We retain title to the goods delivered by us hereinafter: reserved goods
## 8. Payment terms | Offsetting | Right of retention
Our invoices are payable 14 days after invoice date without any deduction. In case of non-compliance with the agreed payment terms, we are entitled to calculate interest at a rate of 7 percentage points above the base interest rate; the right to assert further damages, in particular proven higher interest, remains unaffected by this.
Our invoices are payable 10 days after invoice date without any deduction. Invoicing or date is basically the day of handover to the forwarder as far as we deliver from our German warehouses. Otherwise, in the case of direct imports, the day of customs clearance, which is close to the delivery day, applies. In case of non-compliance with the agreed payment terms, we are entitled to calculate interest at a rate of 7 percentage points above the base interest rate; the right to assert further damages, in particular proven higher interest, remains unaffected by this.
## 9. Delivery reservation | Partial deliveries
@@ -70,13 +69,13 @@ If a call-off order is issued to us and no separate written agreements are made
## 12. Dimension and weight specifications
All information about diameter, weight, technical design, manufacture and scope of the goods to be delivered by us are subject to the reservation of deviation within the commercially usual permissible tolerances. In addition, we reserve changes that serve a technical improvement at any time. Color deviations and/or deviations in the external characteristics of the goods to be delivered by us, which however leave their quality and technical effectiveness unaffected, do not give rise to any claims for defects by the orderer.
All information about diameter, weight, technical design, manufacture and scope of the goods to be delivered by us are subject to the reservation of deviation within the commercially usual permissible tolerances. Furthermore, we reserve the right to make changes that serve technical improvement at any time. Color deviations and/or deviations in the external characteristics of the goods to be delivered by us, which however leave their quality and technical effectiveness unaffected, do not give rise to any claims for defects by the orderer.
## 13. Transfer of risk and burden
Delivery is made DAP free destination Germany, where the place of performance for delivery and any subsequent performance is also located.
If the ordered goods are made ready for shipment by us and/or the dispatch and/or the call-off is delayed for reasons for which the orderer is responsible, we are entitled to demand compensation for the damage resulting therefrom including additional expenses. For this, we calculate a flat-rate compensation of 2% of the invoice amount for each month started, however maximum 10% in total, starting with the delivery period or lacking a delivery period with the notification of readiness for shipment of the goods.
If the ordered goods are made ready for shipment by us and/or the dispatch and/or the call-off is delayed for reasons for which the orderer is responsible, we are entitled to demand compensation for the damage resulting therefrom including additional expenses for storage. For this, we calculate a flat-rate compensation of 2% of the invoice amount for each month started, however maximum 10% in total, starting with the delivery period or lacking a delivery period with the notification of readiness for shipment of the goods.
Proof of higher damage and our legal claims (in particular compensation for additional expenses, reasonable compensation, termination) remain unaffected; the flat-rate is however to be credited against further monetary claims. The orderer is permitted to prove that no damage or only a substantially lower damage than the aforementioned flat-rate has arisen for us. Returns to us, which have not been confirmed by us in writing beforehand, are at the sole risk of the orderer.
@@ -94,7 +93,9 @@ Further claims of the orderer, regardless of the legal basis, are excluded or li
The limitation periods for claims for defects are 24 months from delivery of the goods.
If there are different opinions regarding cable damage in the event of a defect notification, only the expertise of the VDE Institute itself applies in case of doubt. We do not accept other, even accredited test laboratories. We expressly point out that when laying the cable in the trench or in pipes, or in buildings, a constant visual inspection must be carried out by the cable layer to check for any noticeable features. A later complaint that suggests negligent behavior is thus restricted.
If there are different opinions regarding cable damage in the event of a defect notification, only the expertise of the VDE Institute itself applies in case of doubt. We do not accept other, even accredited test laboratories.
We expressly point out that when laying the cable in the trench or in pipes, or in buildings, a constant visual inspection must be carried out by the cable layer to check for any noticeable features. A later complaint that suggests negligent behavior is thus restricted. This also applies to the acceptance of the goods, where obvious damage must be communicated directly. Subsequent claims after acceptance of a faultless delivery must be proven in detail.
## 15. Damages | Total liability
@@ -110,4 +111,6 @@ Only the law of the Federal Republic of Germany applies, excluding the UN Conven
With the publication of these DPT on the Internet, all previously used conditions of ours become void.
Remshalden, January 28, 2026
[Download as PDF](/AGB-KLZ-1-2026.pdf)

View File

@@ -10,6 +10,12 @@ categories:
- Solarkabel
images:
- /uploads/2025/06/H1Z2Z2-K-scaled.webp
application: >
Das H1Z2Z2-K entspricht der Norm DIN EN 50618 (VDE 0283-618) und ist speziell für die
Verkabelung von Photovoltaiksystemen konzipiert. Es kann fest verlegt oder flexibel
geführt werden im Gebäude, im Freien, in Industrieanlagen, landwirtschaftlichen
Betrieben oder sogar in explosionsgefährdeten Bereichen. Die Leitung ist UV-, ozon-
und wasserbeständig (AD7) und darf direkt in der Erde verlegt werden.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Niederspannungskabel
images:
- /uploads/2025/01/N2X2Y-scaled.webp
application: >
Das N2X2Y entspricht den Normen HD 603 S1 Teil 5G und HD 627 S1 Teil 4H (gleichlautend
mit DIN VDE 0276-603 und -627) und ist für eine Betriebsfrequenz von 50Hz ausgelegt.
Es eignet sich für die feste Verlegung in Innenräumen, im Erdreich, im Freien und in
Industrieumgebungen mit hohen Temperatur- und Belastungsanforderungen. Die maximale
Betriebstemperatur liegt bei +90°C, im Kurzschlussfall sind +250°C zulässig.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -9,6 +9,12 @@ description: >
categories:
- Hochspannungskabel
images: []
application: >
Die N2X(F)K2Y-Hochspannungskabelserie ist speziell für den Einsatz in
Hochspannungsanwendungen konzipiert, wobei sie eine optimale Leistung durch die
Verwendung von hochleitfähigem Kupfer und einer fortschrittlichen XLPE-Isolierung
bietet. Diese Kombination gewährleistet eine hohe Durchschlagsfestigkeit und eine
effiziente Thermozyklierung unter verschiedenen Betriebsbedingungen.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -9,6 +9,13 @@ description: >
categories:
- Hochspannungskabel
images: []
application: >
Die N2X(F)KLD2Y-Hochspannungskabel Serie 2 sind speziell für den Einsatz in
Hochspannungsanwendungen konzipiert, wobei sie eine optimale Leistung durch die
Verwendung von hochleitfähigen Kupferleitern und einer fortschrittlichen
XLPE-Isolierung bieten. Diese Kabelserie ist besonders geeignet für anspruchsvolle
industrielle Umgebungen, wo hohe Durchschlagsfestigkeit und
Thermozyklierungsfähigkeit erforderlich sind.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Mittelspannungskabel
images:
- /uploads/2025/01/N2XS2Y-scaled.webp
application: >
Das N2XS2Y erfüllt die Normen DIN VDE 0276-620, HD 620 S2 und IEC 60502. Es eignet sich
zur Verlegung in Innenräumen, Kabelkanälen, im Freien, im Wasser, auf Kabelpritschen
und insbesondere im Erdreich. Aufgrund seines widerstandsfähigen Mantels wird es
häufig in Industrieanlagen, Kraftwerken und Schaltstationen eingesetzt, wo Stabilität
und Langlebigkeit gefordert sind.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Mittelspannungskabel
images:
- /uploads/2025/01/N2XSF2Y-3-scaled.webp
application: >
Das N2XS(F)2Y erfüllt die gängigen Normen DIN VDE 0276-620, HD 620 S2 und IEC 60502 und
ist für die Verlegung in Innenräumen, Kabelkanälen, im Freien, in Wasser, Erde und auf
Kabelpritschen geeignet. Besonders in EVU-Netzen, Industrieanlagen und Kraftwerken
spielt dieses Kabel seine Stärken aus überall dort, wo Langlebigkeit,
Wasserdichtigkeit und Sicherheit gefragt sind.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Hochspannungskabel
images:
- /uploads/2025/06/N2XSFL2Y-3-scaled.webp
application: >
Das N2XS(FL)2Y ist konzipiert für die Verlegung im Erdreich, in Kabelkanälen, in Rohren,
im Freien und in Innenräumen. Es entspricht der Norm IEC 60840 und lässt sich
individuell auf projektspezifische Anforderungen anpassen. Typisch eingesetzt wird es
in Übertragungsnetzen, Umspannwerken und großen Industrieanlagen, wo maximale
Zuverlässigkeit gefordert ist.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Mittelspannungskabel
images:
- /uploads/2025/01/N2XSFL2Y-2-scaled.webp
application: >
Das N2XS(FL)2Y erfüllt die Standards DIN VDE 0276-620, HD 620 S2 und IEC 60502. Es
eignet sich hervorragend für die Verlegung in Innenräumen, Kabelkanälen, im Freien, in
Erde, im Wasser sowie auf Kabelpritschen insbesondere in EVU-Netzen,
Industrieanlagen und Schaltstationen, wo erhöhte Anforderungen an mechanische
Belastbarkeit und Wasserdichtigkeit bestehen.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Mittelspannungskabel
images:
- /uploads/2025/01/N2XSY-scaled.webp
application: >
Das N2XSY erfüllt die Normen DIN VDE 0276-620, HD 620 S2 und IEC 60502. Es ist
ausgelegt für die Verlegung in Innenräumen, Kabelkanälen, im Wasser, im Erdreich oder
im Freien (bei geschützter Installation). Ob in Industrieanlagen, Kraftwerken oder
Schaltanlagen dieses Kabel sorgt für eine sichere und verlustarme
Energieübertragung im Mittelspannungsbereich.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,13 @@ categories:
- Niederspannungskabel
images:
- /uploads/2025/01/N2XY-scaled.webp
application: >
Das N2XY wird in Niederspannungsanlagen zur Energieverteilung eingesetzt zum Beispiel
in Kabeltrassen, Rohren, auf Wänden oder direkt im Erdreich. Es lässt sich sowohl im
Innen- als auch im Außenbereich installieren und ist auch für feuchte Umgebungen
geeignet. Dank verschiedener Aderkonfigurationen (einadrig bis vieradrig) und
Querschnitten bis 630mm² lässt sich das Kabel flexibel an die jeweilige Anwendung
anpassen.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Niederspannungskabel
images:
- /uploads/2025/01/NA2X2Y-scaled.webp
application: >
Das NA2X2Y entspricht der Norm DIN VDE 0276-603 (HD 603) und ist ausgelegt für die
feste Verlegung in Innenräumen, Kabelkanälen, im Erdreich, im Wasser oder im Freien.
Es kommt bevorzugt in Kraftwerken, Industrieanlagen, Schaltanlagen und Ortsnetzen zum
Einsatz überall dort, wo robuste Kabel gefragt sind, die im Betrieb und bei der
Verlegung hohen mechanischen Belastungen standhalten.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -9,6 +9,12 @@ description: >
categories:
- Hochspannungskabel
images: []
application: >
Die NA2X(F)K2Y-Hochspannungskabelserie ist speziell für den Einsatz in
Hochspannungsanwendungen entwickelt worden, wobei sie sich durch eine hohe
Strombelastbarkeit und exzellente Kurzschlussstromfestigkeit auszeichnet. Diese Kabel
sind mit einer fortschrittlichen XLPE-Isolierung ausgestattet, die eine hohe
Durchschlagsfestigkeit and Thermozyklierungsfähigkeit bietet.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -9,6 +9,12 @@ description: >
categories:
- Hochspannungskabel
images: []
application: >
Die NA2X(F)KLD2Y-Hochspannungskabelserie ist für den Einsatz in
Hochspannungsanwendungen konzipiert und bietet eine optimale Lösung für
anspruchsvolle industrielle Umgebungen. Mit ihrer fortschrittlichen
XLPE-Isolierung und Kupferleitern erfüllt sie hohe Anforderungen an die
Durchschlagsfestigkeit und Thermozyklierung.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Mittelspannungskabel
images:
- /uploads/2025/01/NA2XS2Y-scaled.webp
application: >
Das NA2XS2Y erfüllt die Normen DIN VDE 0276-620, HD 620 S2 und IEC 60502 und ist
speziell für die feste Verlegung in Innenräumen, Kabelkanälen, im Freien, in Erde und
in Wasser ausgelegt. Es findet seinen Einsatz in Industrieanlagen, Schaltstationen
und Kraftwerken, besonders dort, wo das Kabel beim Verlegen oder im Betrieb
mechanisch stark beansprucht wird.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Mittelspannungskabel
images:
- /uploads/2025/01/NA2XSF2Y-3-scaled.webp
application: >
Das NA2XS(F)2Y entspricht den Normen DIN VDE 0276-620, HD 620 S2 und IEC 60502. Es
eignet sich für die Verlegung in Innenräumen, Kabelkanälen, im Erdreich, im Wasser, im
Freien oder auf Kabelpritschen. Der Einsatzschwerpunkt liegt in EVU-Netzen,
Industrieanlagen und Umspannwerken, wo zusätzliche Sicherheitsreserven gegen
eindringende Feuchtigkeit und mechanische Belastung erforderlich sind.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Hochspannungskabel
images:
- /uploads/2025/06/NA2XSFL2Y-3-scaled.webp
application: >
Das NA2XS(FL)2Y erfüllt die Anforderungen der IEC 60840 und eignet sich für die
Verlegung im Erdreich, in Kabelkanälen, in Innenräumen, in Rohren und im Freien. Es
wird projektbezogen gefertigt und kommt insbesondere in Übertragungsnetzen,
Versorgungs-Infrastrukturen und Umspannwerken zum Einsatz, wo Sicherheit und
Langlebigkeit an erster Stelle stehen.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,13 @@ categories:
- Mittelspannungskabel
images:
- /uploads/2025/01/NA2XSFL2Y-3-scaled.webp
application: >
Das NA2XS(FL)2Y erfüllt die Normen DIN VDE 0276-620, HD 620 S2 und IEC 60502. Es ist
ideal für die Verlegung in Energieversorgungsnetzen (EVU), Innenräumen, Kabelkanälen,
im Freien, in Erde und in Wasser geeignet. Dank seiner Konstruktion mit
längswasserdichter Ausführung und Alu-PE-Schichtenmantel bleibt es auch bei
Beschädigungen betriebssicher der Wassereinfluss wird gezielt auf die Schadstelle
begrenzt.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -11,6 +11,12 @@ categories:
- Mittelspannungskabel
images:
- /uploads/2025/01/NA2XSY-scaled.webp
application: >
Das NA2XSY erfüllt die Anforderungen der Normen DIN VDE 0276-620, HD 620 S2 und IEC
60502. Es eignet sich für die Verlegung in Innenräumen, Kabelkanälen, im Erdreich, im
Wasser oder im Freien allerdings nur bei geschützter Verlegung. Typische
Einsatzorte sind Industrieanlagen, Kraftwerke und Schaltanlagen, in denen
Mittelspannung mit hoher Betriebssicherheit transportiert werden muss.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,11 @@ categories:
- Niederspannungskabel
images:
- /uploads/2025/01/NA2XY-scaled.webp
application: >
Das NA2XY entspricht der Norm DIN VDE 0276-603 (HD 603) und ist ideal für die feste
Verlegung in Innenräumen, Kabelkanälen, im Freien, im Wasser oder im Erdreich
geeignet. Typische Einsatzorte sind Kraftwerke, Industrieanlagen, Schaltanlagen sowie
Ortsnetze, bei denen mechanische Belastung im Betrieb berücksichtigt werden muss.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -11,6 +11,12 @@ categories:
- Niederspannungskabel
images:
- /uploads/2025/01/NAY2Y-scaled.webp
application: >
Das NAY2Y erfüllt die Anforderungen der Norm TP PRAKAB 12/03 in Anlehnung an VDE
0276-603 und eignet sich für die feste Verlegung in Innenräumen, Kabelkanälen, im
Erdreich, im Wasser und im Außenbereich. Es ist ideal für Anwendungen in Kraftwerken,
Industrie- und Schaltanlagen sowie in lokalen Versorgungsnetzen überall dort, wo
mechanische Belastung im Betrieb oder bei der Verlegung eine Rolle spielt.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Niederspannungskabel
images:
- /uploads/2025/01/NAYCWY-scaled.webp
application: >
Das NAYCWY entspricht der Norm DIN VDE 0276-603 (HD 603) und eignet sich für den
Einsatz in Kraftwerken, Industrieanlagen, Schaltanlagen und Ortsnetzen. Es lässt sich
fest verlegen in Innenräumen, Kabelkanälen, im Freien, im Erdreich oder in Wasser.
Dank des konzentrischen Leiters bietet es zusätzlichen Schutz bei mechanischer
Beschädigung und ermöglicht eine sichere Potenzialführung.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -11,6 +11,12 @@ categories:
- Niederspannungskabel
images:
- /uploads/2025/01/NAYY-scaled.webp
application: >
Das NAYY ist ein Energieverteilungskabel nach VDE 0276-603, das sich besonders für
Anwendungen in Kraftwerken, Ortsnetzen, Industrie- und Schaltanlagen eignet. Dank
seiner robusten Konstruktion lässt es sich fest verlegen sei es im Innenraum, im
Kabelkanal, im Freien oder im Erdreich. Auch bei Installation in Wasser bleibt das
Kabel zuverlässig im Betrieb.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Niederspannungskabel
images:
- /uploads/2025/01/NY2Y-scaled.webp
application: >
Das NY2Y ist ein Niederspannungskabel für den Einsatz in Kraftwerken, Industrie- und
Schaltanlagen sowie in Ortsnetzen. Es eignet sich für die feste Verlegung in
Innenräumen, Kabelkanälen, im Freien, im Wasser und im Erdreich überall dort, wo
starke mechanische Belastungen beim Verlegen und im Betrieb zu erwarten sind. Die
Konstruktion erfüllt die Vorgaben gemäß TP PRAKAB 16/03 in Anlehnung an VDE 0276-603.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,13 @@ categories:
- Niederspannungskabel
images:
- /uploads/2025/01/NYCWY-scaled.webp
application: >
Das NYCWY gehört zu den klassischen Niederspannungskabeln nach VDE-Standard und ist
für Nennspannungen bis 1kV ausgelegt. Es kommt überall dort zum Einsatz, wo Energie
zuverlässig verteilt werden muss in Gebäuden, Industrieanlagen, Trafostationen oder
direkt im Erdreich. Auch in Kabeltrassen, Betonumgebungen oder unter Wasser lässt es
sich problemlos verlegen. Die Materialwahl sorgt dafür, dass dieses Kabel selbst
unter rauen Bedingungen durchhält ganz ohne zusätzliche Schutzmaßnahmen.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -9,6 +9,9 @@ categories:
- Niederspannungskabel
images:
- /uploads/2025/01/NYY-scaled.webp
application: |
Verwendung
Das Kabel entspricht den Normen DIN VDE 0276-603. Es eignet sich für die Verlegung in Innenräumen, Kabelkanälen, im Erdreich, im Wasser, im Freien oder auf Kabelpritschen, sofern keine besonderen mechanischen Beanspruchungen zu erwarten sind. Der Einsatzschwerpunkt liegt in Kraftwerken, Industrieanlagen und Ortsnetzen.
locale: de
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -9,6 +9,12 @@ categories:
- Solar Cables
images:
- /uploads/2025/06/H1Z2Z2-K-scaled.webp
application: >
The H1Z2Z2-K complies with DIN EN 50618 (VDE 0283-618) and is specifically designed for
the cabling of photovoltaic systems. It can be installed permanently or used flexibly
indoors, outdoors, in industrial facilities, agricultural operations, or even in
hazardous (explosive) areas. The cable is UV-, ozone- and water-resistant (AD7) and
can be laid directly in the ground.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Low Voltage Cables
images:
- /uploads/2025/01/N2X2Y-scaled.webp
application: >
The N2X2Y complies with HD 603 S1 Part 5G and HD 627 S1 Part 4H (equivalent to DIN VDE
0276-603 and -627) and is designed for an operating frequency of 50 Hz. It is suitable
for fixed installation indoors, underground, outdoors, and in industrial environments
with high temperature and load requirements. The maximum operating temperature is
+90 °C, and +250 °C is permissible under short-circuit conditions.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ description: >
categories:
- High Voltage Cables
images: []
application: >
The N2X(F)K2Y high-voltage cable series is tailored for robust performance in
high-voltage power systems, featuring copper conductors and cross-linked
polyethylene (XLPE) insulation. This combination ensures high dielectric strength
and excellent thermal cycling resistance, crucial for maintaining integrity and
functionality over long operational periods.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -8,6 +8,11 @@ description: >
categories:
- High Voltage Cables
images: []
application: >
The N2X(F)KLD2Y-high-voltage-cables-2 series is engineered to meet the rigorous
demands of high-voltage power transmission with a focus on durability, efficiency,
and safety. This series is ideal for applications requiring high dielectric
strength and excellent thermal cycling resistance.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Medium Voltage Cables
images:
- /uploads/2025/01/N2XS2Y-scaled.webp
application: >
The N2XS2Y complies with the standards DIN VDE 0276-620, HD 620 S2 and IEC 60502. It is
suitable for installation indoors, in cable ducts, outdoors, in water, on cable
trays, and especially underground. Thanks to its robust sheath, it is frequently
used in industrial plants, power stations, and switching stations, where stability
and durability are essential.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Medium Voltage Cables
images:
- /uploads/2025/01/N2XSF2Y-3-scaled.webp
application: >
The N2XSF2Y complies with common standards DIN VDE 0276-620, HD 620 S2 and IEC 60502,
and is suitable for installation indoors, in cable ducts, outdoors, in water,
underground, and on cable trays. This cable proves its strengths especially in
utility grids, industrial plants, and power stations wherever durability,
watertightness, and safety are essential.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- High Voltage Cables
images:
- /uploads/2025/06/N2XSFL2Y-3-scaled.webp
application: >
The N2XS(FL)2Y is designed for installation in the ground, in cable ducts, pipes,
outdoor areas, and indoor spaces. It complies with the IEC 60840 standard and can be
tailored to specific project requirements. It is typically used in transmission
networks, substations, and large industrial facilities where maximum reliability is
essential.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,11 @@ categories:
- Medium Voltage Cables
images:
- /uploads/2025/01/N2XSFL2Y-2-scaled.webp
application: >
The N2XS(FL)2Y meets the standards DIN VDE 0276-620, HD 620 S2 and IEC 60502. It is
ideally suited for installation indoors, in cable ducts, outdoors, in soil, in water,
and on cable trays especially in utility grids, industrial plants, and switching
stations, where high demands on mechanical strength and water resistance apply.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,11 @@ categories:
- Medium Voltage Cables
images:
- /uploads/2025/01/N2XSY-scaled.webp
application: >
The N2XSY meets the standards DIN VDE 0276-620, HD 620 S2 and IEC 60502. It is designed
for installation indoors, in cable ducts, in water, underground, or outdoors (when
protected). Whether in industrial plants, power stations, or substations this cable
ensures safe and low-loss power transmission in medium-voltage networks.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Low Voltage Cables
images:
- /uploads/2025/01/N2XY-scaled.webp
application: >
The N2XY is used in low-voltage systems for power distribution for example in cable
trays, conduits, on walls, or directly underground. It can be installed both indoors
and outdoors and is also suitable for humid environments. Thanks to various core
configurations (single-core to four-core) and cross-sections up to 630 mm², the cable
can be flexibly adapted to the respective application.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Low Voltage Cables
images:
- /uploads/2025/01/NA2X2Y-scaled.webp
application: >
The NA2X2Y complies with DIN VDE 0276-603 (HD 603) and is designed for fixed
installation indoors, in cable ducts, underground, in water, or outdoors. It is
primarily used in power plants, industrial facilities, and switching stations as well
as local distribution networks wherever robust cables are needed that can withstand
high mechanical stress during installation and operation.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -9,6 +9,11 @@ description: >
categories:
- High Voltage Cables
images: []
application: >
The NA2X(F)K2Y high-voltage cable series is engineered to meet the rigorous demands
of modern industrial and power distribution applications. It combines high-grade
copper conductors with cross-linked polyethylene (XLPE) insulation, ensuring
superior dielectric strength and thermal cycling resilience.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -9,6 +9,11 @@ description: >
categories:
- High Voltage Cables
images: []
application: >
The NA2X(F)KLD2Y high-voltage cable series is engineered to meet the rigorous
demands of modern industrial electrical networks, offering high dielectric
strength and excellent thermal cycling resistance. This series is ideal for
applications that require reliable power distribution in high-voltage settings.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

View File

@@ -10,6 +10,12 @@ categories:
- Medium Voltage Cables
images:
- /uploads/2025/01/NA2XS2Y-scaled.webp
application: >
The NA2XS2Y complies with DIN VDE 0276-620, HD 620 S2 and IEC 60502 standards, and is
specifically designed for fixed installation indoors, in cable ducts, outdoors, in
soil and water. It is commonly used in industrial facilities, switching stations, and
power plants, especially where the cable is exposed to high mechanical stress during
installation or operation.
locale: en
---
<ProductTabs technicalData={<ProductTechnicalData data={{

Some files were not shown because too many files have changed in this diff Show More