Compare commits

...

62 Commits

Author SHA1 Message Date
76cbdaa37e fix: remove class 1 conductor shapes (RE, SE) from all NA cables
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 9s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-08-07 14:49:27 +02:00
5b1a2eff7f fix(mdx): handle productTabs in MDXContent to resolve Unknown Block Type
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 9s
Build & Deploy / 🧪 QA (push) Successful in 1m13s
Build & Deploy / 🏗️ Build (push) Successful in 2m15s
Build & Deploy / 🚀 Deploy (push) Successful in 16s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m22s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-08-07 12:54:02 +02:00
60422da644 chore(glitchtip): ignore version drift errors like ChunkLoadError globally
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 9m9s
Build & Deploy / 🧪 QA (push) Successful in 1m19s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-07-21 12:23:52 +02:00
fc6da4e9aa fix(mdx): Handle brace counting correctly inside strings for Next-MDX-Remote data props parsing
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Successful in 1m13s
Build & Deploy / 🏗️ Build (push) Successful in 2m31s
Build & Deploy / 🚀 Deploy (push) Successful in 16s
Build & Deploy / 🔔 Notify (push) Successful in 3s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m18s
2026-07-18 09:43:33 +02:00
f7411d3dc4 2.3.30
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Successful in 1m13s
Build & Deploy / 🏗️ Build (push) Successful in 2m31s
Build & Deploy / 🚀 Deploy (push) Successful in 16s
Build & Deploy / 🔔 Notify (push) Successful in 2s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m21s
2026-07-17 15:54:38 +02:00
7be2ec605c fix(contact): correct pino logger argument order to pass typescript compiler 2026-07-17 15:54:35 +02:00
bd00175912 2.3.29
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 1m18s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-17 15:18:50 +02:00
9c9663eb88 fix(mdx): robust parsing for payload data and ignore sentry pipe noise 2026-07-17 15:18:35 +02:00
402b5c30c6 chore: trigger deployment with new Mailgun credentials
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 12s
Build & Deploy / 🧪 QA (push) Failing after 1m28s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-27 08:55:09 +02:00
2bb0381de4 chore: revert deploy.yml fallback, using Gitea variables now
Some checks failed
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Has been cancelled
2026-06-27 08:24:40 +02:00
614b0a5ffd feat(contact): add klaus@klz-cables.com to recipients and fix pino logging
Some checks failed
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Has been cancelled
2026-06-27 08:20:34 +02:00
b1569a1ce2 chore: bump version to 2.3.29-rc.1
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 13s
Build & Deploy / 🧪 QA (push) Successful in 1m26s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-22 12:02:28 +02:00
2c35c6b8aa feat(og): add logo and dynamic faded background image to OG templates
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-22 11:41:32 +02:00
bff150b1a7 feat: add new business card assets to public downloads directory
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 12s
Build & Deploy / 🧪 QA (push) Successful in 1m32s
Build & Deploy / 🏗️ Build (push) Successful in 3m15s
Build & Deploy / 🚀 Deploy (push) Successful in 20s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m38s
Build & Deploy / 🔔 Notify (push) Successful in 2s
Nightly QA / 💨 Smoke & Health (push) Successful in 1m6s
2026-06-22 09:23:15 +02:00
8ffb1e5f6a chore(release): 2.3.28 - Add Gotify notifications
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Successful in 1m27s
Build & Deploy / 🏗️ Build (push) Successful in 3m15s
Build & Deploy / 🚀 Deploy (push) Successful in 18s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m30s
Build & Deploy / 🔔 Notify (push) Successful in 3s
Nightly QA / 💨 Smoke & Health (push) Successful in 1m23s
2026-06-21 09:34:05 +02:00
42ff5b6226 chore(release): 2.3.27
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Successful in 1m29s
Build & Deploy / 🏗️ Build (push) Successful in 2m49s
Build & Deploy / 🚀 Deploy (push) Successful in 17s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
Build & Deploy / 🔔 Notify (push) Successful in 2s
Nightly QA / 💨 Smoke & Health (push) Successful in 3m16s
2026-06-20 21:51:28 +02:00
539c503d0d fix(blog): remove unused metadata import causing lint warning 2026-06-20 21:51:11 +02:00
4c473a2ffd chore(release): 2.3.26
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 0s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Failing after 3s
2026-06-20 09:32:53 +02:00
4f64a94a15 fix(blog): make category filter badges functional and preserve filter in pagination 2026-06-20 09:32:42 +02:00
5a82243965 fix(pdf): calculate exact page size points to avoid bleed zone gap for Saxoprint
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Successful in 55s
Build & Deploy / 🏗️ Build (push) Successful in 1m53s
Build & Deploy / 🚀 Deploy (push) Successful in 15s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m3s
Build & Deploy / 🔔 Notify (push) Successful in 1s
Nightly QA / 💨 Smoke & Health (push) Successful in 1m17s
2026-06-14 09:44:50 +02:00
ebb14ee302 feat(pdf): add Saxoprint CMYK variant (no mask, no pantone)
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Successful in 53s
Build & Deploy / 🏗️ Build (push) Successful in 3m14s
Build & Deploy / 🚀 Deploy (push) Successful in 14s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m6s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-14 09:37:31 +02:00
80f5e46d88 fix(pdf): fix logo color override on front side
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Successful in 1m19s
Build & Deploy / 🏗️ Build (push) Successful in 2m42s
Build & Deploy / 🚀 Deploy (push) Successful in 18s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m22s
Build & Deploy / 🔔 Notify (push) Successful in 4s
Nightly QA / 💨 Smoke & Health (push) Successful in 2m6s
2026-06-12 16:08:18 +02:00
b826b5b741 fix(pdf): change pantone fallback function to type 2 for better viewer compatibility
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 15s
Build & Deploy / 🧪 QA (push) Successful in 1m50s
Build & Deploy / 🏗️ Build (push) Successful in 2m51s
Build & Deploy / 🚀 Deploy (push) Successful in 18s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m24s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 15:44:54 +02:00
41228aca53 fix(pdf): remove letterpress mask and set correct logo color for white page
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 12s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-12 15:42:14 +02:00
cd3b7c1e3b style(pdf): convert all text and icons on front side to klz-blau
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 13s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-12 15:38:26 +02:00
ecd525d782 feat(pdf): build stream-hacking pipeline for 5-color pantone 289 c export
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Successful in 1m20s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-12 15:35:19 +02:00
52439cc5aa chore(release): bump version to 2.3.25
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 12s
Build & Deploy / 🧪 QA (push) Successful in 1m20s
Build & Deploy / 🏗️ Build (push) Successful in 2m37s
Build & Deploy / 🚀 Deploy (push) Successful in 17s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m7s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 14:58:31 +02:00
61bf8517d8 fix(pdf): add explicit type assertions to avoid TS2698 on spread
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m39s
Build & Deploy / 🚀 Deploy (push) Successful in 19s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m23s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 14:48:52 +02:00
85d9e3a930 fix(pdf): revert address footer bottom to 7mm
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Failing after 1m21s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 14:45:51 +02:00
2f7d6befe4 fix(pdf): reduce gap under role and increase footer distance from bottom edge
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Failing after 1m22s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
2026-06-12 14:44:07 +02:00
a6ba484faa fix(pdf): increase role text spacing and link agbs pdf directly to static file
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Failing after 1m20s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-12 14:11:46 +02:00
d7178a9100 chore: clean up garbage files from business card generation and update PDFs
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Failing after 1m19s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 13:31:53 +02:00
1e39dc8567 fix(klz-2026): correctly hide tagline via path overrides without breaking KLZ letters
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Failing after 1m20s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 13:24:24 +02:00
3c2aa8b71e fix(klz-2026): remove tagline from front page (white) while preserving it on back page (blue)
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 14s
Build & Deploy / 🧪 QA (push) Successful in 1m52s
Build & Deploy / 🏗️ Build (push) Successful in 2m53s
Build & Deploy / 🚀 Deploy (push) Successful in 18s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m25s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 13:10:38 +02:00
6e3befdfd9 fix(klz-2026): increase top padding to push header and content down on business card back
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 14s
Build & Deploy / 🧪 QA (push) Successful in 1m50s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-12 13:07:46 +02:00
5377b3c81a feat(klz-2026): use external URL and static .vcf files for ultra-minimalist QR codes
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 13s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-12 13:05:24 +02:00
941625a4e0 feat(klz-2026): use MECARD and level L for smaller, highly scannable QR codes on business cards
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Successful in 1m20s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-12 13:01:49 +02:00
9b251101d4 chore(release): 2.3.24 - deploy AVB fix
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 14s
Build & Deploy / 🧪 QA (push) Successful in 2m2s
Build & Deploy / 🏗️ Build (push) Successful in 3m49s
Build & Deploy / 🚀 Deploy (push) Successful in 19s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m26s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 12:11:45 +02:00
ff0d880bde style: shift text block upwards on white page for more distance to address
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 13s
Build & Deploy / 🧪 QA (push) Successful in 1m41s
Build & Deploy / 🏗️ Build (push) Successful in 2m44s
Build & Deploy / 🚀 Deploy (push) Successful in 19s
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
2026-06-12 12:05:05 +02:00
b78a848ec2 fix: ensure email addresses are strictly lowercase
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 18s
Build & Deploy / 🧪 QA (push) Successful in 1m59s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-12 12:01:00 +02:00
e72879b222 style: change primary blue to website dark blue and set white page logo to black
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 24s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-12 11:57:48 +02:00
5a2b0c543e style: remove truck illustration completely and increase bottom margin for print safety
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Successful in 1m41s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-12 11:52:48 +02:00
1203c20166 fix: remove unintended truck background from the white contact page
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 16s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-06-12 11:51:53 +02:00
9944d3313b fix: correct background color of second page back to white
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Failing after 1m56s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 9s
2026-06-12 11:48:20 +02:00
d1cbfc41c8 feat: add crop marks variant for print finishing
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 24s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-06-12 11:47:40 +02:00
09d435a1bc fix: align address footer flush left with contact icon column
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 QA (push) Failing after 1m52s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 12s
2026-06-12 11:35:44 +02:00
8f34ac2a6d style: fine-tune white page typography - harmonize font sizes, spacing rhythm and letter-spacing
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🧪 QA (push) Failing after 1m54s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 12s
2026-06-12 11:32:44 +02:00
e5ecf16a66 fix: revert line spacing to compact layout, keep generous logo margin
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 22s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-12 11:30:39 +02:00
12eadba0ee fix: resolve PDF business card layout overlap and aspect ratio distortion
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 22s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-06-12 11:29:34 +02:00
0ad975f049 style: crisp white icon labels (remove opacity), Bodemer role updated, tagline lowercase
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 24s
Build & Deploy / 🧪 QA (push) Failing after 1m54s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 13s
2026-06-12 11:09:19 +02:00
b53ea82be8 fix: make brand text and icons pure white on blue background for better contrast, fix white page layout 2026-06-12 11:04:50 +02:00
3d7a061858 style: remove hard dark blue footer bar, add elegant green separator line
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Failing after 1m52s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 12s
2026-06-12 10:49:04 +02:00
44ad62de1c style: increase bottom bar height to 16mm to give the tagline more breathing room
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 12s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-06-12 10:47:12 +02:00
092325baa9 style: redesign blue page - compact 14mm bar, refined typography proportions, premium negative space
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Failing after 1m20s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 10:41:19 +02:00
a0910dd932 style: optimize print design harmony for blue page
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Failing after 1m22s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 10:38:37 +02:00
db251687d1 fix: resolve address footer overlap and restore 4mm symmetrical margins
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Failing after 1m22s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 10:36:19 +02:00
eaf17315ff fix: implement strict print grid and optical centering for business card
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Failing after 1m21s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 10:30:55 +02:00
6bab5c9173 revert 2026-06-12 10:29:05 +02:00
cf371e48ef fix(klz-2026): remove bottom margins from AVB components to fix spacing issues
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Failing after 1m24s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 10:25:21 +02:00
ec0c71a6a6 feat: add NoTruck variant, center layout padding, match green colors
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Failing after 1m20s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-12 10:21:56 +02:00
1b7fecff4a fix(klz-2026): add missing agbHistory and pdfDownload components to MDXContent to fix AVB page, remove invalid clipRule from TruckBlueprint
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 13s
Build & Deploy / 🧪 QA (push) Successful in 1m20s
Build & Deploy / 🏗️ Build (push) Successful in 2m40s
Build & Deploy / 🚀 Deploy (push) Successful in 18s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m25s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 09:49:07 +02:00
b7f2d932a9 fix: resolve address footer page break and make QR border green
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Failing after 1m20s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 09:44:51 +02:00
59 changed files with 1431 additions and 902 deletions

View File

@@ -256,6 +256,10 @@ jobs:
# Analytics
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
# Notifications
GOTIFY_URL: ${{ secrets.GOTIFY_URL }}
GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -309,6 +313,10 @@ jobs:
echo "UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID"
echo "UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT"
echo ""
echo "# Notifications"
echo "GOTIFY_URL=$GOTIFY_URL"
echo "GOTIFY_TOKEN=$GOTIFY_TOKEN"
echo ""
echo "TARGET=$TARGET"
echo "SENTRY_ENVIRONMENT=$TARGET"
echo "PROJECT_NAME=$PROJECT_NAME"

1
.gitignore vendored
View File

@@ -28,3 +28,4 @@ backups/
# Local data / backups
.data/
scratch/

View File

@@ -1,7 +1,7 @@
import { ImageResponse } from 'next/og';
import { getPageBySlug } from '@/lib/pages';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png';
@@ -20,9 +20,12 @@ export default async function Image({
}
const fonts = await getOgFonts();
const { logo, bgImage } = getOgAssets();
return new ImageResponse(
<OGImageTemplate
image={bgImage}
logo={logo}
title={pageData.frontmatter.title}
description={pageData.frontmatter.excerpt}
label="Information"

View File

@@ -1,7 +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';
import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
import { SITE_URL } from '@/lib/schema';
export const size = OG_IMAGE_SIZE;
@@ -35,6 +35,7 @@ export default async function Image({
}
const fonts = await getOgFonts();
const { logo, bgImage } = getOgAssets();
// 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
@@ -55,10 +56,11 @@ export default async function Image({
return new ImageResponse(
<OGImageTemplate
logo={logo}
title={post.frontmatter.title}
description={post.frontmatter.excerpt}
label={post.frontmatter.category || 'Blog'}
image={base64Image || featuredImage}
image={base64Image || featuredImage || bgImage}
/>,
{
...OG_IMAGE_SIZE,

View File

@@ -1,7 +1,7 @@
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';
import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png';
@@ -11,9 +11,16 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Blog.meta' });
const fonts = await getOgFonts();
const { logo, bgImage } = getOgAssets();
return new ImageResponse(
<OGImageTemplate title={t('title')} description={t('description')} label="Blog" />,
<OGImageTemplate
image={bgImage}
logo={logo}
title={t('title')}
description={t('description')}
label="Blog"
/>,
{
...OG_IMAGE_SIZE,
fonts,

View File

@@ -3,7 +3,7 @@ import Image from 'next/image';
import { getAllPosts } from '@/lib/blog';
import { Section, Container, Heading, Card, Badge, Button } from '@/components/ui';
import Reveal from '@/components/Reveal';
import { Metadata } from 'next';
import { getTranslations, setRequestLocale } from 'next-intl/server';
import { SITE_URL } from '@/lib/schema';
import { BlogPaginationKeyboardObserver } from '@/components/blog/BlogPaginationKeyboardObserver';
@@ -12,6 +12,7 @@ interface BlogIndexProps {
params: Promise<{
locale: string;
}>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
export async function generateMetadata({ params }: BlogIndexProps) {
@@ -41,8 +42,10 @@ export async function generateMetadata({ params }: BlogIndexProps) {
};
}
export default async function BlogIndex({ params }: BlogIndexProps) {
export default async function BlogIndex({ params, searchParams }: BlogIndexProps) {
const { locale } = await params;
const resolvedSearchParams = await searchParams;
const categoryParam = resolvedSearchParams.category as string | undefined;
setRequestLocale(locale);
const t = await getTranslations('Blog');
const posts = await getAllPosts(locale);
@@ -52,8 +55,14 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
(a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime(),
);
const featuredPost = sortedPosts[0];
const remainingPosts = sortedPosts.slice(1);
const filteredPosts = categoryParam
? sortedPosts.filter(
(post) => post.frontmatter.category?.toLowerCase() === categoryParam.toLowerCase(),
)
: sortedPosts;
const featuredPost = filteredPosts[0];
const remainingPosts = filteredPosts.slice(1);
return (
<div className="bg-neutral-light min-h-screen">
@@ -123,31 +132,38 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
{t('allArticles')}
</Heading>
<div className="flex flex-wrap gap-2 md:gap-4">
{/* Category filters could go here */}
<Link href={`/${locale}/blog`}>
<Badge
variant="primary"
variant={!categoryParam ? 'primary' : 'neutral'}
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
>
{t('categories.all')}
</Badge>
</Link>
<Link href={`/${locale}/blog?category=industry`}>
<Badge
variant="neutral"
variant={categoryParam === 'industry' ? 'primary' : 'neutral'}
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
>
{t('categories.industry')}
</Badge>
</Link>
<Link href={`/${locale}/blog?category=technical`}>
<Badge
variant="neutral"
variant={categoryParam === 'technical' ? 'primary' : 'neutral'}
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
>
{t('categories.technical')}
</Badge>
</Link>
<Link href={`/${locale}/blog?category=sustainability`}>
<Badge
variant="neutral"
variant={categoryParam === 'sustainability' ? 'primary' : 'neutral'}
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
>
{t('categories.sustainability')}
</Badge>
</Link>
</div>
</div>
</Reveal>
@@ -260,7 +276,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
{t('prev')}
</Button>
<Button
href={`/${locale}/blog?page=1`}
href={`/${locale}/blog?page=1${categoryParam ? `&category=${categoryParam}` : ''}`}
variant="primary"
size="sm"
className="md:h-11 md:px-6 md:text-base"
@@ -269,7 +285,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
1
</Button>
<Button
href={`/${locale}/blog?page=2`}
href={`/${locale}/blog?page=2${categoryParam ? `&category=${categoryParam}` : ''}`}
variant="outline"
size="sm"
className="md:h-11 md:px-6 md:text-base"
@@ -277,7 +293,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
2
</Button>
<Button
href={`/${locale}/blog?page=2`}
href={`/${locale}/blog?page=2${categoryParam ? `&category=${categoryParam}` : ''}`}
variant="outline"
size="sm"
className="md:h-11 md:px-6 md:text-base"

View File

@@ -1,7 +1,7 @@
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';
import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png';
@@ -11,12 +11,19 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Contact' });
const fonts = await getOgFonts();
const { logo, bgImage } = getOgAssets();
const title = t('meta.title') || t('title');
const description = t('meta.description') || t('subtitle');
return new ImageResponse(
<OGImageTemplate title={title} description={description} label="Contact" />,
<OGImageTemplate
image={bgImage}
logo={logo}
title={title}
description={description}
label="Contact"
/>,
{
...OG_IMAGE_SIZE,
fonts,

View File

@@ -1,7 +1,7 @@
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';
import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png';
@@ -11,9 +11,12 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Index.meta' });
const fonts = await getOgFonts();
const { logo, bgImage } = getOgAssets();
return new ImageResponse(
<OGImageTemplate
image={bgImage}
logo={logo}
title={t('title')}
description={t('description')}
label="Reliable Energy Infrastructure"

View File

@@ -1,7 +1,7 @@
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';
import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png';
@@ -11,6 +11,7 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Products' });
const fonts = await getOgFonts();
const { logo, bgImage } = getOgAssets();
const title = t.has('meta.title')
? t('meta.title')
@@ -20,7 +21,13 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
const description = t('meta.description') || t('subtitle');
return new ImageResponse(
<OGImageTemplate title={title} description={description} label="Products" />,
<OGImageTemplate
image={bgImage}
logo={logo}
title={title}
description={description}
label="Products"
/>,
{
...OG_IMAGE_SIZE,
fonts,

View File

@@ -1,7 +1,7 @@
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';
import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png';
@@ -11,12 +11,19 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Team' });
const fonts = await getOgFonts();
const { logo, bgImage } = getOgAssets();
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" />,
<OGImageTemplate
image={bgImage}
logo={logo}
title={title}
description={description}
label="Our Team"
/>,
{
...OG_IMAGE_SIZE,
fonts,

View File

@@ -61,7 +61,7 @@ export async function sendContactFormAction(formData: FormData) {
productName,
message,
};
fs.appendFileSync(backupFile, JSON.stringify(leadData) + '\\n');
fs.appendFileSync(backupFile, JSON.stringify(leadData) + '\n');
logger.info('Successfully saved lead to local backup file', { backupFile });
} catch (backupError) {
logger.error('Failed to write to local leads backup', { error: String(backupError) });
@@ -156,7 +156,7 @@ export async function sendContactFormAction(formData: FormData) {
// Notify via Gotify (Internal)
await services.notifications.notify({
title: `📩 ${notificationSubject}`,
title: `📩 [KLZ] ${notificationSubject}`,
message: `New message from ${name} (${email}):\n\n${message}`,
priority: 5,
});

View File

@@ -22,6 +22,9 @@ import MeetTheTeam from './home/MeetTheTeam';
import GallerySection from './home/GallerySection';
import VideoSection from './home/VideoSection';
import CTA from './home/CTA';
import { AgbHistoryBlock } from './AgbHistoryBlock';
import { PDFDownloadBlock } from './PDFDownloadBlock';
import ProductTechnicalData from './ProductTechnicalData';
function ContactSection(props: any) {
return (
<div className="p-8 border-2 border-dashed border-primary my-8 text-center text-primary font-bold">
@@ -94,6 +97,12 @@ async function Block(props: any) {
return <VideoSection data={data} />;
case 'homeCTA':
return <CTA data={data} />;
case 'agbHistory':
return <AgbHistoryBlock {...data} />;
case 'pdfDownload':
return <PDFDownloadBlock {...data} />;
case 'productTabs':
return <ProductTechnicalData data={data} />;
default:
return (

View File

@@ -5,6 +5,7 @@ interface OGImageTemplateProps {
description?: string;
label?: string;
image?: string;
logo?: string;
mode?: 'dark' | 'light' | 'image';
}
@@ -13,6 +14,7 @@ export function OGImageTemplate({
description,
label,
image,
logo,
mode = 'dark',
}: OGImageTemplateProps) {
const primaryBlue = '#001a4d';
@@ -152,9 +154,13 @@ export function OGImageTemplate({
height: '6px',
backgroundColor: accentGreen,
borderRadius: '3px',
marginRight: '24px',
marginRight: '32px',
}}
/>
{logo ? (
/* eslint-disable-next-line @next/next/no-img-element */
<img src={logo} alt="KLZ Cables" height="40" style={{ objectFit: 'contain' }} />
) : (
<div
style={{
fontSize: '24px',
@@ -167,6 +173,7 @@ export function OGImageTemplate({
>
KLZ Cables
</div>
)}
</div>
{/* Saturated Blue Brand Strip */}

View File

@@ -3,7 +3,11 @@
import React from 'react';
import { usePathname } from 'next/navigation';
export const PDFDownloadBlock: React.FC<{ label: string; style: string }> = ({ label, style }) => {
export const PDFDownloadBlock: React.FC<{ label: string; style: string; url?: string }> = ({
label,
style,
url,
}) => {
const pathname = usePathname();
// Extract slug from pathname
@@ -12,7 +16,7 @@ export const PDFDownloadBlock: React.FC<{ label: string; style: string }> = ({ l
// We want the page slug.
const slug = segments[segments.length - 1] || 'home';
const href = `/api/pages/${slug}/pdf`;
const href = url || `/api/pages/${slug}/pdf`;
return (
<div className="my-8">

View File

@@ -247,7 +247,7 @@ _status: published
Stand: April 2026
<Block type="pdfDownload" data={{"id":"69a9b09ea220fecfb9d54bb8","label":"Als PDF herunterladen","style":"primary","blockName":"","blockType":"pdfDownload"}} />
<Block type="pdfDownload" data={{"id":"69a9b09ea220fecfb9d54bb8","label":"Als PDF herunterladen","style":"primary","url":"/AVB-KLZ-4-2026.pdf","blockName":"","blockType":"pdfDownload"}} />
<Block type="agbHistory" data={{"id":"69a9b09ea220fecfb9d54bb9","blockName":"AGB History","blockType":"agbHistory","title":"Vorherige Versionen"}} />

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -65,11 +65,8 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostD
const fileContent = await fs.readFile(filePath, 'utf-8');
const { data, content } = matter(fileContent);
// Fix MDX data props dropped by next-mdx-remote
// Payload serializes as data={{"items":...}} which Acorn treats as a Block statement.
const fixedContent = content.replace(/data=\{\{([\s\S]*?)\}\}/g, (match, p1) => {
return 'data="{' + p1.replace(/"/g, '&quot;') + '}"';
});
const { fixMdxDataProps } = await import('./mdx-utils');
const fixedContent = fixMdxDataProps(content);
let parsedContent = fixedContent;
try {

68
lib/mdx-utils.ts Normal file
View File

@@ -0,0 +1,68 @@
export function fixMdxDataProps(content: string): string {
let fixedContent = content;
let dataIndex = fixedContent.indexOf('data={{');
while (dataIndex !== -1) {
let openCount = 0;
let endIndex = -1;
const startObj = dataIndex + 6; // index of the first '{' in 'data={{'
let inString = false;
let isEscaped = false;
for (let i = startObj; i < fixedContent.length; i++) {
const char = fixedContent[i];
if (isEscaped) {
isEscaped = false;
continue;
}
if (char === '\\') {
isEscaped = true;
continue;
}
if (char === '"') {
inString = !inString;
continue;
}
if (!inString) {
if (char === '{') {
openCount++;
} else if (char === '}') {
openCount--;
if (openCount === 0) {
endIndex = i;
break;
}
}
}
}
if (endIndex !== -1) {
// jsonStr is the content INSIDE the outer curly braces
const jsonStr = fixedContent.substring(startObj + 1, endIndex);
const safeJsonStr = jsonStr.replace(/"/g, '&quot;');
const replacement = `data="{${safeJsonStr}}"`;
// We also need to consume the closing `}` of the `data={{`
const nextCharIndex = endIndex + 1;
const skipChars =
nextCharIndex < fixedContent.length && fixedContent[nextCharIndex] === '}' ? 2 : 1;
fixedContent =
fixedContent.substring(0, dataIndex) +
replacement +
fixedContent.substring(endIndex + skipChars);
dataIndex = fixedContent.indexOf('data={{', dataIndex + replacement.length);
} else {
// Malformed braces, prevent infinite loop
break;
}
}
return fixedContent;
}

View File

@@ -55,3 +55,36 @@ export const OG_IMAGE_SIZE = {
width: 1200,
height: 630,
};
/**
* Loads a local image and converts it to a base64 data URI for Satori
*/
export function getLocalImageBase64(filename: string): string | undefined {
try {
const filePath = join(process.cwd(), 'public', filename);
const buffer = readFileSync(filePath);
const ext = filename.split('.').pop()?.toLowerCase();
const mimeType =
ext === 'png'
? 'image/png'
: ext === 'jpg' || ext === 'jpeg'
? 'image/jpeg'
: 'image/svg+xml';
return `data:${mimeType};base64,${buffer.toString('base64')}`;
} catch (error) {
console.error(`[OG] Failed to load local image ${filename}:`, error);
return undefined;
}
}
/**
* Returns standard assets for OG images
*/
export function getOgAssets() {
return {
logo: getLocalImageBase64('logo-white.png'),
bgImage: getLocalImageBase64(
'media/aerial-view-of-electricity-station-surrounded-with-2023-11-27-05-33-40-utc-scaled.jpg',
),
};
}

View File

@@ -53,11 +53,8 @@ export async function getPageBySlug(slug: string, locale: string): Promise<PageD
const fileContent = await fs.readFile(filePath, 'utf-8');
const { data, content } = matter(fileContent);
// Fix MDX data props dropped by next-mdx-remote
// Payload serializes as data={{"items":...}} which Acorn treats as a Block statement.
const fixedContent = content.replace(/data=\{\{([\s\S]*?)\}\}/g, (match, p1) => {
return 'data="{' + p1.replace(/"/g, '&quot;') + '}"';
});
const { fixMdxDataProps } = await import('./mdx-utils');
const fixedContent = fixMdxDataProps(content);
let parsedContent = fixedContent;
try {

View File

@@ -47,14 +47,15 @@ export interface PDFBusinessCardProps {
qrCodeDataUrl: string;
variant?: 'blue' | 'white' | 'black';
isViaprinto?: boolean;
withTruck?: boolean;
withCropMarks?: boolean;
}
// Die Buchstaben K, L, Z im SVG (inklusive dem kleinen Stück vom Z oben links)
export const KLZ_PATHS = [25, 26, 27, 28, 29];
// Mintel Aesthetics Colors - EXACT PROD COLORS
const COLORS = {
primary: '#011dff', // Brand Royal Blue
primary: '#000a66', // Brand Dark Blue (from website)
blue: '#000a66',
green: '#82ed20', // Brand Accent Green
darkGreen: '#4A9E00', // Darker green for white background visibility
@@ -65,6 +66,98 @@ const COLORS = {
lightGray: '#f8f9fa',
};
const CropMarks = () => (
<View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, zIndex: 9999 }}>
{/* Top Left */}
<View
style={{
position: 'absolute',
left: '13mm',
top: '0mm',
width: '0.25pt',
height: '10mm',
backgroundColor: '#000',
}}
/>
<View
style={{
position: 'absolute',
left: '0mm',
top: '13mm',
width: '10mm',
height: '0.25pt',
backgroundColor: '#000',
}}
/>
{/* Top Right */}
<View
style={{
position: 'absolute',
right: '13mm',
top: '0mm',
width: '0.25pt',
height: '10mm',
backgroundColor: '#000',
}}
/>
<View
style={{
position: 'absolute',
right: '0mm',
top: '13mm',
width: '10mm',
height: '0.25pt',
backgroundColor: '#000',
}}
/>
{/* Bottom Left */}
<View
style={{
position: 'absolute',
left: '13mm',
bottom: '0mm',
width: '0.25pt',
height: '10mm',
backgroundColor: '#000',
}}
/>
<View
style={{
position: 'absolute',
left: '0mm',
bottom: '13mm',
width: '10mm',
height: '0.25pt',
backgroundColor: '#000',
}}
/>
{/* Bottom Right */}
<View
style={{
position: 'absolute',
right: '13mm',
bottom: '0mm',
width: '0.25pt',
height: '10mm',
backgroundColor: '#000',
}}
/>
<View
style={{
position: 'absolute',
right: '0mm',
bottom: '13mm',
width: '10mm',
height: '0.25pt',
backgroundColor: '#000',
}}
/>
</View>
);
const styles = StyleSheet.create({
pageBleed: {
width: '91mm',
@@ -89,7 +182,7 @@ const styles = StyleSheet.create({
top: 0,
left: 0,
right: 0,
bottom: '18mm', // Leave space for the bar below
bottom: '20mm', // Truck stops above the footer area to avoid text conflict
zIndex: 1,
},
frontBackgroundImage: {
@@ -99,78 +192,76 @@ const styles = StyleSheet.create({
},
frontContainer: {
flex: 1,
paddingBottom: '18mm', // Push the logo up to center it in the area above the bar
paddingTop: '3mm', // Compensate for top bleed for perfect optical centering
paddingBottom: '20mm', // Leave space for the footer area
justifyContent: 'center',
alignItems: 'center',
},
frontLogo: {
width: 175,
width: 145, // Reduced for a more elegant, premium look with more negative space
zIndex: 100,
},
frontTagline: {
color: COLORS.white,
fontSize: 6,
marginTop: 10,
letterSpacing: 1.5,
fontSize: 5.5,
marginTop: 8,
letterSpacing: 2,
fontWeight: 'bold',
textTransform: 'uppercase',
zIndex: 100,
},
frontGreenBarBottom: {
frontFooterArea: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: '18mm',
borderTop: `2pt solid ${COLORS.green}`,
backgroundColor: '#000a66',
height: '22mm', // Increased height to accommodate more bottom padding
paddingBottom: '10mm', // Increased bottom padding to move icons further from edge
zIndex: 50,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: 5,
gap: 6, // Harmonious spacing to icons
},
frontIconsGroup: {
flexDirection: 'row',
gap: 12,
gap: 14, // Wider gaps between icon groups for visual clarity at small size
},
frontBrandIconWrapper: {
alignItems: 'center',
gap: 2,
gap: 1.5,
},
frontBrandIcon: {
width: 18,
height: 18,
opacity: 0.9,
width: 12, // Smaller for compact bar
height: 12,
opacity: 0.85,
},
frontBrandText: {
fontSize: 5.5,
fontSize: 4, // Tiny but readable at 300dpi print
color: COLORS.white,
textTransform: 'uppercase',
letterSpacing: 0.5,
opacity: 0.9,
letterSpacing: 0.6,
// Full opacity — white on blue needs to pop cleanly
},
frontTaglineBar: {
color: COLORS.green,
fontSize: 7,
letterSpacing: 1.5,
fontSize: 7, // Larger — as requested
letterSpacing: 0.5, // Harmonious tracking for mixed case
fontWeight: 'bold',
textTransform: 'uppercase',
},
// -- WHITE PAGE (Vorderseite) --
backContainer: {
flex: 1,
paddingTop: '6mm',
paddingBottom: '6mm',
paddingTop: '6mm', // Pushed down as requested
paddingLeft: '6mm',
paddingRight: '6mm',
flexDirection: 'column',
},
backHeader: {
marginBottom: '5mm',
flexDirection: 'row',
alignItems: 'center',
marginBottom: '5mm', // Generous breathing room
width: '100%',
},
backLogo: {
@@ -179,9 +270,9 @@ const styles = StyleSheet.create({
accentLineHorizontal: {
flex: 1,
height: 1.5,
backgroundColor: '#011dff', // Brand blue line
marginLeft: 15,
marginRight: 10,
backgroundColor: COLORS.primary, // Brand blue line
marginLeft: 12,
marginRight: 12,
},
accentDot: {
width: 4,
@@ -201,40 +292,40 @@ const styles = StyleSheet.create({
},
backBody: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'flex-start',
width: '100%',
},
leftColumn: {
flex: 1,
width: '100%', // Take full width since right column is removed
flexDirection: 'column',
paddingRight: '4mm',
},
nameSection: {
marginBottom: '4mm',
marginBottom: '2.5mm', // Reduced to fix the 'Lücke' under the role
},
nameText: {
fontSize: 14,
fontSize: 13,
fontWeight: 'bold',
color: COLORS.neutralDark,
marginBottom: 2,
letterSpacing: -0.2,
color: COLORS.primary,
marginBottom: 1.5,
letterSpacing: -0.15,
},
roleText: {
fontSize: 7.5,
fontSize: 6.5,
fontWeight: 'bold',
color: '#011dff',
letterSpacing: 0.5,
color: COLORS.darkGreen,
letterSpacing: 1.2,
textTransform: 'uppercase',
},
contactSection: {
flexDirection: 'column',
gap: 4,
gap: 0,
},
contactRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 5, // Even rhythm between contact lines
},
iconWrapper: {
width: 12,
@@ -242,10 +333,11 @@ const styles = StyleSheet.create({
alignItems: 'flex-start',
},
contactText: {
fontSize: 8,
color: COLORS.neutralDark,
fontSize: 7.5,
color: COLORS.primary,
fontWeight: 'normal',
paddingTop: 1,
paddingTop: 0.5,
letterSpacing: 0.1,
},
rightColumn: {
flexDirection: 'column',
@@ -259,10 +351,10 @@ const styles = StyleSheet.create({
gap: 3,
},
qrCodeWrapper: {
width: '12mm',
height: '12mm',
border: '0.75pt solid #011dff',
padding: 2.5,
width: '10mm',
height: '10mm',
border: `0.75pt solid ${COLORS.darkGreen}`,
padding: 2,
backgroundColor: '#ffffff',
},
qrCode: {
@@ -272,16 +364,16 @@ const styles = StyleSheet.create({
addressFooter: {
width: '100%',
flexDirection: 'row',
justifyContent: 'center',
justifyContent: 'flex-start',
alignItems: 'center',
paddingTop: '2.5mm',
marginTop: 'auto',
marginBottom: '2mm', // Force extra space from the bottom edge
position: 'absolute',
bottom: '7mm', // 3mm bleed + 4mm safe margin
left: '6mm', // Match backContainer paddingLeft for flush alignment
},
addressFooterText: {
fontSize: 7, // increased from 6
color: COLORS.grayText,
letterSpacing: 0.3,
fontSize: 6.5,
color: COLORS.primary,
letterSpacing: 0.4,
fontWeight: 'normal',
},
});
@@ -291,7 +383,7 @@ const PhoneIcon = () => (
<Svg viewBox="0 0 24 24" width={9} height={9}>
<Path
d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z"
fill="#000000"
fill={COLORS.primary}
/>
</Svg>
);
@@ -300,7 +392,7 @@ const EmailIcon = () => (
<Svg viewBox="0 0 24 24" width={9} height={9}>
<Path
d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"
fill="#000000"
fill={COLORS.primary}
/>
</Svg>
);
@@ -309,7 +401,7 @@ const GlobeIcon = () => (
<Svg viewBox="0 0 24 24" width={9} height={9}>
<Path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"
fill="#000000"
fill={COLORS.primary}
/>
</Svg>
);
@@ -318,7 +410,7 @@ const LocationIcon = () => (
<Svg viewBox="0 0 24 24" width={9} height={9}>
<Path
d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"
fill="#000000"
fill={COLORS.primary}
/>
</Svg>
);
@@ -327,7 +419,7 @@ const ScanIcon = () => (
<Svg viewBox="0 0 24 24" width={9} height={9}>
<Path
d="M3 3h6v2H5v4H3V3zm18 0h-6v2h4v4h2V3zM3 21h6v-2H5v-4H3v6zm18 0h-6v-2h4v-4h2v6zM7 7h10v10H7V7z"
fill="#000000"
fill={COLORS.primary}
/>
</Svg>
);
@@ -363,6 +455,7 @@ const VectorLogo = ({
style,
overrideColor,
overrideWhiteColor,
overrideBlackColor,
pathOverrides,
solidPaths,
solidBackgroundColor = '#000a66',
@@ -371,6 +464,7 @@ const VectorLogo = ({
style: any;
overrideColor?: string;
overrideWhiteColor?: string;
overrideBlackColor?: string;
pathOverrides?: Record<number, string>;
solidPaths?: number[];
solidBackgroundColor?: string;
@@ -400,6 +494,11 @@ const VectorLogo = ({
(p.fill === 'white' || p.fill === '#ffffff' || p.fill === '#fff')
) {
fill = overrideWhiteColor;
} else if (
overrideBlackColor &&
(p.fill === 'black' || p.fill === '#000000' || p.fill === '#0a0a0a' || p.fill === '#000')
) {
fill = overrideBlackColor;
}
return <Path key={i} d={d} fill={fill} />;
})}
@@ -632,7 +731,7 @@ export const IconHighVoltage = ({ style, color = 'white' }: { style: any; color?
style={{ fill: 'none', stroke: color, strokeWidth: '0.33px' }}
/>
<Path
d="M28.597,16.426c-0,-1.03 -0.837,-1.867 -1.868,-1.867c-1.03,0 -1.867,0.837 -1.867,1.867c0,1.031 0.837,1.868 1.867,1.868c1.031,-0 1.868,-0.837 1.868,-1.868Z"
d="M28.597,16.426c-0,-1.03 -0.837,-1.868 -1.868,-1.867c-1.03,0 -1.867,0.837 -1.867,1.867c0,1.031 0.837,1.868 1.867,1.868c1.031,-0 1.868,-0.837 1.868,-1.868Z"
style={{ fill: 'none', stroke: color, strokeWidth: '0.33px' }}
/>
<Path
@@ -693,6 +792,8 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
qrCodeDataUrl,
variant = 'blue',
isViaprinto = false,
withTruck = true,
withCropMarks = false,
}) => {
const isWhiteVariant = variant === 'white' || variant === 'black';
@@ -721,14 +822,37 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
};
const getBackLogoPaths = () => {
if (variant === 'black') return logoBlackWithTaglinePaths;
return logoBlueWithTaglinePaths;
// We use the original black logo to preserve the exact colors of the Truck and KLZ.
// The tagline will be hidden via pathOverrides in the JSX.
return logoBlackWithTaglinePaths;
};
const bgImagePath = isWhiteVariant
? path.join(process.cwd(), 'public', 'media', 'cable_drums_truck.png')
: path.join(process.cwd(), 'public', 'cable_drums_bg.png');
const mmToPt = (mm: number) => (mm * 72) / 25.4;
const renderPageWrapper = (children: React.ReactNode, bleedStyle: any) => {
if (withCropMarks) {
return (
<Page size={[315, 230]} style={{ backgroundColor: '#ffffff' }}>
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<CropMarks />
<View style={[bleedStyle, { overflow: 'hidden', position: 'relative' }]}>
{children}
</View>
</View>
</Page>
);
}
return (
<Page size={[mmToPt(91), mmToPt(61)]} style={bleedStyle}>
{children}
</Page>
);
};
return (
<Document
title={`Business Card - ${person.name}`}
@@ -736,25 +860,11 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
creator="KLZ Print Engine"
>
{/* VORDERSEITE (Blue page with logo, truck and tagline) */}
<Page
size={[258, 173]}
style={[styles.pageBleed, isWhiteVariant && { backgroundColor: '#ffffff' }]}
>
<View style={styles.truckWrapper}>
<TruckBlueprint
style={styles.frontBackgroundImage}
opacity={isWhiteVariant ? 0.2 : 0.25}
backgroundColor={isWhiteVariant ? '#ffffff' : '#011dff'}
/>
</View>
<View
style={[
styles.frontGreenBarBottom,
isWhiteVariant && { backgroundColor: '#ffffff', borderTopColor: COLORS.darkGreen },
]}
>
{renderPageWrapper(
<>
<View style={styles.frontFooterArea}>
<Text style={[styles.frontTaglineBar, isWhiteVariant && { color: COLORS.black }]}>
VON 0,6/1,0 KV BIS 64/110 KV
Von 0,6/1,0 kV bis 64/110 kV
</Text>
<View style={styles.frontIconsGroup}>
<View style={styles.frontBrandIconWrapper}>
@@ -802,34 +912,35 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
paths={getFrontLogoPaths()}
style={styles.frontLogo}
solidPaths={KLZ_PATHS}
solidBackgroundColor={isWhiteVariant ? '#ffffff' : '#011dff'}
solidBackgroundColor={isWhiteVariant ? '#ffffff' : COLORS.primary}
/>
{/* Letterpress Mask Layer: We use #ff00fe as a marker color. Only the "KLZ" letters! */}
{isViaprinto && (
<VectorLogo
paths={getFrontLogoPaths().filter((_, i) => KLZ_PATHS.includes(i))}
style={[styles.frontLogo, { position: 'absolute' }]}
overrideColor="#ff00fe"
solidPaths={KLZ_PATHS.map((_, i) => i)}
/>
)}
</View>
</Page>
</>,
[styles.pageBleed, isWhiteVariant && { backgroundColor: '#ffffff' }],
)}
{/* VORDERSEITE (White page with contact info) */}
<Page size={[258, 173]} style={styles.pageBleedBack}>
{renderPageWrapper(
<View style={styles.backContainer}>
<View style={styles.backHeader}>
<VectorLogo
paths={getBackLogoPaths()}
style={styles.backLogo}
overrideWhiteColor={'#011dff'}
pathOverrides={KLZ_PATHS.reduce((acc, idx) => ({ ...acc, [idx]: 'none' }), {})}
overrideBlackColor={COLORS.primary}
pathOverrides={{
...(KLZ_PATHS.reduce(
(acc: Record<number, string>, idx) => ({ ...acc, [idx]: 'none' }),
{} as Record<number, string>,
) as any),
// Paths 0-24 are the "Cables for a greener tomorrow" tagline in the 40-path logo.
...(Array.from({ length: 25 }).reduce(
(acc: Record<number, string>, _, i) => ({ ...acc, [i]: 'none' }),
{} as Record<number, string>,
) as any),
}}
/>
<View style={styles.accentLineHorizontal} />
<View style={styles.qrContainer}>
<View style={styles.qrCodeWrapper}>
<View style={[styles.qrCodeWrapper, { borderColor: COLORS.darkGreen }]}>
<Image src={qrCodeDataUrl} style={styles.qrCode} />
</View>
</View>
@@ -853,7 +964,7 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
<View style={styles.iconWrapper}>
<EmailIcon />
</View>
<Text style={styles.contactText}>{person.email}</Text>
<Text style={styles.contactText}>{person.email.toLowerCase()}</Text>
</View>
<View style={styles.contactRow}>
<View style={styles.iconWrapper}>
@@ -864,14 +975,15 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
</View>
</View>
</View>
</View>
<View style={styles.addressFooter}>
<Text style={styles.addressFooterText}>
KLZ Vertriebs GmbH · Raiffeisenstraße 22 · 73630 Remshalden
</Text>
</View>
</Page>
</View>,
styles.pageBleedBack,
)}
</Document>
);
};

View File

@@ -329,6 +329,42 @@ const renderLexicalNode = (node: any, idx: number): React.ReactNode => {
}
};
const renderMarkdown = (markdown: string) => {
const blocks = markdown.split('\n\n').filter((b) => b.trim());
return blocks.map((block, idx) => {
if (block.startsWith('### ')) {
return (
<Text key={idx} style={styles.heading3} wrap={false} minPresenceAhead={100}>
{block.replace('### ', '').trim()}
</Text>
);
}
if (block.startsWith('## ')) {
return (
<Text key={idx} style={styles.heading2} wrap={false} minPresenceAhead={100}>
{block.replace('## ', '').trim()}
</Text>
);
}
if (block.startsWith('# ')) {
return (
<View key={idx} style={styles.heading1Wrapper} wrap={false} minPresenceAhead={100}>
<Text style={styles.heading1}>{block.replace('# ', '').trim()}</Text>
</View>
);
}
// Ignore block components like <Block ... />
if (block.startsWith('<Block')) return null;
return (
<Text key={idx} style={styles.paragraph} minPresenceAhead={15}>
{block.trim()}
</Text>
);
});
};
interface PDFPageProps {
page: any;
locale?: string;
@@ -360,7 +396,9 @@ export const PDFPage: React.FC<PDFPageProps> = ({ page, locale = 'de' }) => {
<View style={styles.content}>
<View>
{page.content?.root?.children?.map((node: any, i: number) =>
{typeof page.content === 'string'
? renderMarkdown(page.content)
: page.content?.root?.children?.map((node: any, i: number) =>
renderLexicalNode(node, i),
)}
</View>

View File

@@ -39,11 +39,8 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
const fileContent = await fs.readFile(filePath, 'utf-8');
const { data, content } = matter(fileContent);
// Fix MDX data props dropped by next-mdx-remote
// Payload serializes as data={{"items":...}} which Acorn treats as a Block statement.
const fixedContent = content.replace(/data=\{\{([\s\S]*?)\}\}/g, (match, p1) => {
return 'data="{' + p1.replace(/"/g, '&quot;') + '}"';
});
const { fixMdxDataProps } = await import('./mdx-utils');
const fixedContent = fixMdxDataProps(content);
let parsedContent = fixedContent;
try {

View File

@@ -56,6 +56,11 @@ export class GlitchtipErrorReportingService implements ErrorReportingService {
tracesSampleRate: this.options.tracesSampleRate ?? 0.1,
replaysOnErrorSampleRate: 1.0,
replaysSessionSampleRate: 0.1,
ignoreErrors: [
'ChunkLoadError',
'Failed to fetch dynamically imported module',
'Failed to find Server Action',
],
});
}
return Sentry;

View File

@@ -116,7 +116,7 @@
"prepare": "husky",
"preinstall": "npx only-allow pnpm"
},
"version": "2.3.23",
"version": "2.3.32",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -0,0 +1,10 @@
BEGIN:VCARD
VERSION:3.0
N:Gleich;Johannes;;;
FN:Johannes Gleich
ORG:KLZ Vertriebs GmbH
TITLE:Senior Key Account Manager
TEL;TYPE=WORK,VOICE:+49 172 739 1109
EMAIL;TYPE=PREF,INTERNET:johannes.gleich@klz-cables.com
URL:https://klz-cables.com
END:VCARD

10
public/v/klausmintel.vcf Normal file
View File

@@ -0,0 +1,10 @@
BEGIN:VCARD
VERSION:3.0
N:Mintel;Klaus;;;
FN:Klaus Mintel
ORG:KLZ Vertriebs GmbH
TITLE:Managing Director
TEL;TYPE=WORK,VOICE:+49 151 1775 2873
EMAIL;TYPE=PREF,INTERNET:klaus.mintel@klz-cables.com
URL:https://klz-cables.com
END:VCARD

View File

@@ -0,0 +1,10 @@
BEGIN:VCARD
VERSION:3.0
N:Bodemer;Michael;;;
FN:Michael Bodemer
ORG:KLZ Vertriebs GmbH
TITLE:Owner & Managing Director
TEL;TYPE=WORK,VOICE:+49 151 462 467 98
EMAIL;TYPE=PREF,INTERNET:michael.bodemer@klz-cables.com
URL:https://klz-cables.com
END:VCARD

View File

@@ -26,7 +26,7 @@ const CONFIG = {
const PEOPLE: PersonData[] = [
{
name: 'Michael Bodemer',
role: 'Managing Director',
role: 'Owner & Managing Director',
phone: '+49 151 462 467 98',
email: 'michael.bodemer@klz-cables.com',
},
@@ -34,7 +34,7 @@ const PEOPLE: PersonData[] = [
name: 'Klaus Mintel',
role: 'Managing Director',
phone: '+49 151 1775 2873',
email: 'Klaus.mintel@klz-cables.com',
email: 'klaus.mintel@klz-cables.com',
},
{
name: 'Johannes Gleich',
@@ -52,24 +52,26 @@ async function generateBusinessCards() {
for (const person of PEOPLE) {
console.log(`\nGenerating Business Card for: ${person.name}`);
// Generate VCard QR Code
const lastName = person.name.split(' ').slice(1).join(' ');
const firstName = person.name.split(' ')[0];
const vcard = `BEGIN:VCARD
VERSION:3.0
N:${lastName};${firstName};;;
FN:${person.name}
ORG:KLZ Vertriebs GmbH
TITLE:${person.role}
TEL;TYPE=WORK,VOICE:${person.phone}
EMAIL;TYPE=PREF,INTERNET:${person.email}
URL:https://klz-cables.com
END:VCARD`;
const qrCodeDataUrl = await QRCode.toDataURL(vcard, {
// To make the QR code extremely coarse (minimalist), we cannot encode all contact details into it directly.
// Instead, we create a physical .vcf file and encode ONLY its URL into the QR Code (e.g. 35 chars vs 140 chars).
const shortId = person.name.toLowerCase().replace(/[^a-z0-9]/g, ''); // "michaelbodemer"
const vcfUrl = `https://klz-cables.com/v/${shortId}.vcf`;
const vcardFull = `BEGIN:VCARD\r\nVERSION:3.0\r\nN:${lastName};${firstName};;;\r\nFN:${person.name}\r\nORG:KLZ Vertriebs GmbH\r\nTITLE:${person.role}\r\nTEL;TYPE=WORK,VOICE:${person.phone}\r\nEMAIL;TYPE=PREF,INTERNET:${person.email}\r\nURL:https://klz-cables.com\r\nEND:VCARD`;
// Save the physical .vcf file so it can be served by Next.js from /public/v/
const vcardDir = path.join(process.cwd(), 'public/v');
if (!fs.existsSync(vcardDir)) fs.mkdirSync(vcardDir, { recursive: true });
fs.writeFileSync(path.join(vcardDir, `${shortId}.vcf`), vcardFull);
const qrCodeDataUrl = await QRCode.toDataURL(vcfUrl, {
width: 400,
margin: 0,
color: { dark: '#000000', light: '#ffffff' },
errorCorrectionLevel: 'L', // Low error correction = less dots
color: { dark: '#000a66', light: '#ffffff' },
});
const safeName = person.name.replace(/\s+/g, '_');
@@ -94,7 +96,7 @@ END:VCARD`;
// Convert fonts to outlines via ghostscript
const gsCommand = `gs -dNoOutputFonts -sDEVICE=pdfwrite -o "${finalPath}" "${tempPath}"`;
execSync(gsCommand, { stdio: 'inherit' });
fs.unlinkSync(tempPath);
// fs.unlinkSync(tempPath);
console.log(`✓ Final Print File generated (Fonts to Paths, RGB preserved): ${finalFileName}`);
// 2. VIAPRINTO PDF (Letterpress mask, CMYK)
@@ -119,10 +121,79 @@ END:VCARD`;
try {
execSync(cmykCommand, { stdio: 'inherit' });
fs.unlinkSync(tempViaprintoPath);
console.log(`✓ CMYK Print File generated: ${finalFileNameCMYK}`);
console.log(`[INFO] Injecting PANTONE 289 C Spot Color...`);
const injectPantoneCommand = `npx tsx scripts/inject-pantone.ts "${finalPathCMYK}"`;
execSync(injectPantoneCommand, { stdio: 'inherit' });
console.log(`✓ 5-Color Print File generated: ${finalFileNameCMYK}`);
} catch (e) {
console.error(`❌ CMYK conversion failed! Is Ghostscript installed?`);
}
// 2.5 SAXOPRINT PDF (Pure CMYK, no mask, no Pantone)
console.log(`- Rendering Saxoprint PDF (Pure CMYK)...`);
const tempSaxoprintPath = path.join(CONFIG.outputDir, `temp_saxoprint_${safeName}.pdf`);
buffer = await renderToBuffer(
React.createElement(PDFBusinessCard, {
person,
qrCodeDataUrl,
variant: 'blue',
}),
);
fs.writeFileSync(tempSaxoprintPath, buffer);
const finalFileNameSaxoCMYK = `KLZ_Visitenkarte_${safeName}_Saxoprint_CMYK_91x61mm_3mmBleed.pdf`;
const finalPathSaxoCMYK = path.join(CONFIG.outputDir, finalFileNameSaxoCMYK);
console.log(`- Converting to CMYK for Saxoprint...`);
const cmykSaxoCommand = `bash "${CONFIG.ghostscriptScript}" "${tempSaxoprintPath}" "${finalPathSaxoCMYK}"`;
try {
execSync(cmykSaxoCommand, { stdio: 'inherit' });
fs.unlinkSync(tempSaxoprintPath);
console.log(`✓ 4-Color CMYK Print File generated: ${finalFileNameSaxoCMYK}`);
} catch (e) {
console.error(`❌ CMYK conversion failed! Is Ghostscript installed?`);
}
// 3. NO TRUCK PDF (RGB)
console.log(`- Rendering No Truck Variant (RGB)...`);
const noTruckFileName = `KLZ_Visitenkarte_${safeName}_NoTruck.pdf`;
const tempNoTruckPath = path.join(CONFIG.outputDir, `temp_${noTruckFileName}`);
const finalNoTruckPath = path.join(CONFIG.outputDir, noTruckFileName);
buffer = await renderToBuffer(
React.createElement(PDFBusinessCard, {
person,
qrCodeDataUrl,
variant: 'blue',
isViaprinto: false,
withTruck: false,
}),
);
fs.writeFileSync(tempNoTruckPath, buffer);
const gsNoTruckCommand = `gs -dNoOutputFonts -sDEVICE=pdfwrite -o "${finalNoTruckPath}" "${tempNoTruckPath}"`;
execSync(gsNoTruckCommand, { stdio: 'inherit' });
fs.unlinkSync(tempNoTruckPath);
// 4. CROP MARKS PDF (RGB)
console.log(`- Rendering Crop Marks Variant (RGB)...`);
const cropMarksFileName = `KLZ_Visitenkarte_${safeName}_CropMarks.pdf`;
const tempCropMarksPath = path.join(CONFIG.outputDir, `temp_${cropMarksFileName}`);
const finalCropMarksPath = path.join(CONFIG.outputDir, cropMarksFileName);
buffer = await renderToBuffer(
React.createElement(PDFBusinessCard, {
person,
qrCodeDataUrl,
variant: 'blue',
isViaprinto: false,
withCropMarks: true,
}),
);
fs.writeFileSync(tempCropMarksPath, buffer);
const gsCropMarksCommand = `gs -dNoOutputFonts -sDEVICE=pdfwrite -o "${finalCropMarksPath}" "${tempCropMarksPath}"`;
execSync(gsCropMarksCommand, { stdio: 'inherit' });
fs.unlinkSync(tempCropMarksPath);
console.log(`✓ Crop Marks Print File generated: ${cropMarksFileName}`);
}
}

105
scripts/inject-pantone.ts Normal file
View File

@@ -0,0 +1,105 @@
import { PDFDocument, PDFName, decodePDFRawStream, PDFRawStream } from 'pdf-lib';
import fs from 'fs';
import path from 'path';
async function run() {
const inputPath = process.argv[2];
const outputPath = process.argv[3] || inputPath;
if (!inputPath || !fs.existsSync(inputPath)) {
console.error(`[ERROR] Input file not found: ${inputPath}`);
process.exit(1);
}
console.log(`[INFO] Injecting PANTONE 289 C into: ${path.basename(inputPath)}`);
const pdfBytes = fs.readFileSync(inputPath);
const pdfDoc = await PDFDocument.load(pdfBytes);
// CMYK tint transform for PANTONE 289 C.
// Approximation: 100C, 64M, 0Y, 60K (1.0 0.64 0.0 0.60)
// The function maps an input x (0 to 1) to CMYK: x*1.0, x*0.64, x*0.0, x*0.60
const tintTransform = pdfDoc.context.obj({
FunctionType: 2,
Domain: [0, 1],
C0: [0, 0, 0, 0],
C1: [1.0, 0.64, 0.0, 0.6],
N: 1,
});
// Create the /Separation color space array
const pantoneCS = pdfDoc.context.obj([
PDFName.of('Separation'),
PDFName.of('PANTONE#20289#20C'),
PDFName.of('DeviceCMYK'),
tintTransform,
]);
const csRef = pdfDoc.context.register(pantoneCS);
const pages = pdfDoc.getPages();
for (const page of pages) {
let resources = page.node.Resources();
if (!resources) {
resources = pdfDoc.context.obj({});
page.node.set(PDFName.of('Resources'), resources);
}
// Ensure ColorSpace dict exists
let colorSpaces = resources.get(PDFName.of('ColorSpace')) as any;
if (!colorSpaces) {
colorSpaces = pdfDoc.context.obj({});
resources.set(PDFName.of('ColorSpace'), colorSpaces);
}
// Register the PANTONE color space as /CSPantone in the page's resources
colorSpaces.set(PDFName.of('CSPantone'), csRef);
// Read all content streams
const contents = page.node.Contents();
let streams: any[] = [];
if (contents) {
const resolvedContents = pdfDoc.context.lookup(contents);
if (resolvedContents instanceof PDFRawStream) {
streams = [resolvedContents];
} else if (resolvedContents.constructor.name === 'PDFArray') {
const arr = resolvedContents as any;
for (let j = 0; j < arr.size(); j++) {
streams.push(pdfDoc.context.lookup(arr.get(j)));
}
}
}
let newStr = '';
for (let i = 0; i < streams.length; i++) {
const stream = streams[i];
if (stream instanceof PDFRawStream) {
const decoded = decodePDFRawStream(stream).decode();
const str = Buffer.from(decoded).toString('utf-8');
newStr += str + '\n';
}
}
// The CMYK values for the KLZ Blue created by Ghostscript from #000a66
// is '1 0.98 0.224 0.298'. We use a slightly dynamic regex to catch small precision diffs.
const dynamicRegexK = /1 0\.98[0-9]* 0\.224[0-9]* 0\.298[0-9]* k/g;
const dynamicRegexStrokeK = /1 0\.98[0-9]* 0\.224[0-9]* 0\.298[0-9]* K/g;
let modified = false;
if (dynamicRegexK.test(newStr) || dynamicRegexStrokeK.test(newStr)) {
newStr = newStr.replace(dynamicRegexK, '/CSPantone cs 1 scn');
newStr = newStr.replace(dynamicRegexStrokeK, '/CSPantone CS 1 SCN');
modified = true;
}
if (modified) {
const newStream = pdfDoc.context.flateStream(newStr);
const newRef = pdfDoc.context.register(newStream);
page.node.set(PDFName.of('Contents'), newRef);
console.log(`[INFO] Spot color injected into page ${pages.indexOf(page) + 1}.`);
}
}
const finalPdfBytes = await pdfDoc.save();
fs.writeFileSync(outputPath, finalPdfBytes);
console.log(`[SUCCESS] Saved 5-color PDF to ${outputPath}`);
}
run().catch(console.error);

View File

@@ -12,4 +12,5 @@ Sentry.init({
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
ignoreErrors: ['failed to pipe response'],
});

View File

@@ -12,4 +12,5 @@ Sentry.init({
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
ignoreErrors: ['failed to pipe response'],
});

View File

@@ -0,0 +1,47 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { GlitchtipErrorReportingService } from '../lib/services/errors/glitchtip-error-reporting-service';
// Mock the LoggerService
const mockLogger = {
child: vi.fn().mockReturnThis(),
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
} as any;
const mockSentryInit = vi.fn();
vi.mock('@sentry/nextjs', () => ({
init: (...args: any[]) => mockSentryInit(...args),
captureException: vi.fn(),
captureMessage: vi.fn(),
}));
describe('GlitchtipErrorReportingService', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should ignore version drift errors (ChunkLoadError and Server Action) in Sentry config', async () => {
// Simulate window to force client-side init behavior instantly
const originalWindow = global.window;
global.window = {
requestIdleCallback: (cb: Function) => cb(),
} as any;
const service = new GlitchtipErrorReportingService({ enabled: true }, mockLogger);
// Give the dynamic import a moment to resolve
await new Promise((resolve) => setTimeout(resolve, 50));
expect(mockSentryInit).toHaveBeenCalledOnce();
const initCallArgs = mockSentryInit.mock.calls[0][0];
expect(initCallArgs.ignoreErrors).toBeDefined();
expect(initCallArgs.ignoreErrors).toContain('ChunkLoadError');
expect(initCallArgs.ignoreErrors).toContain('Failed to find Server Action');
global.window = originalWindow;
});
});

View File

@@ -0,0 +1,14 @@
import { describe, it, expect } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
describe('MDXContent Mapping', () => {
it('should have productTabs in the switch statement to prevent Unknown Block Type errors', () => {
const filePath = path.join(process.cwd(), 'components/MDXContent.tsx');
const fileContent = fs.readFileSync(filePath, 'utf-8');
// Check if the switch case for productTabs exists
expect(fileContent).toContain("case 'productTabs':");
expect(fileContent).toContain('<ProductTechnicalData');
});
});

22
tests/mdx-regex.test.ts Normal file
View File

@@ -0,0 +1,22 @@
import { describe, it, expect } from 'vitest';
import { fixMdxDataProps } from '../lib/mdx-utils';
describe('MDX Data Props Fixer', () => {
it('should correctly parse MDX with nested JSON containing }}', () => {
// This string simulates the exact structure that causes the bug in n2x2y.mdx
const mdxInput = `<Block type="productTabs" data={{"content":{"root":{"children":[]}},"id":"123"}} />`;
const result = fixMdxDataProps(mdxInput);
const expected = `<Block type="productTabs" data="{&quot;content&quot;:{&quot;root&quot;:{&quot;children&quot;:[]}},&quot;id&quot;:&quot;123&quot;}" />`;
expect(result).toBe(expected);
});
it('should not prematurely truncate if } appears inside a string literal', () => {
const mdxInput = `<Block type="productTabs" data={{"content":{"text":"}>"}}} />`;
const result = fixMdxDataProps(mdxInput);
const expected = `<Block type="productTabs" data="{&quot;content&quot;:{&quot;text&quot;:&quot;}&gt;&quot;}}" />`;
expect(result).toBe(expected.replace('&gt;', '>'));
});
});