Compare commits

..

60 Commits

Author SHA1 Message Date
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
52 changed files with 883 additions and 372 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"

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 */}
<Badge
variant="primary"
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
>
{t('categories.all')}
</Badge>
<Badge
variant="neutral"
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
>
{t('categories.industry')}
</Badge>
<Badge
variant="neutral"
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
>
{t('categories.technical')}
</Badge>
<Badge
variant="neutral"
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
>
{t('categories.sustainability')}
</Badge>
<Link href={`/${locale}/blog`}>
<Badge
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={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={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={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,8 @@ 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';
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 +96,10 @@ 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} />;
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,21 +154,26 @@ export function OGImageTemplate({
height: '6px',
backgroundColor: accentGreen,
borderRadius: '3px',
marginRight: '24px',
marginRight: '32px',
}}
/>
<div
style={{
fontSize: '24px',
fontWeight: 700,
color: 'white',
textTransform: 'uppercase',
letterSpacing: '0.15em',
display: 'flex',
}}
>
KLZ Cables
</div>
{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',
fontWeight: 700,
color: 'white',
textTransform: 'uppercase',
letterSpacing: '0.15em',
display: 'flex',
}}
>
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

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,100 +860,87 @@ 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 },
]}
>
<Text style={[styles.frontTaglineBar, isWhiteVariant && { color: COLORS.black }]}>
VON 0,6/1,0 KV BIS 64/110 KV
</Text>
<View style={styles.frontIconsGroup}>
<View style={styles.frontBrandIconWrapper}>
<IconLowVoltage
style={styles.frontBrandIcon}
color={isWhiteVariant ? COLORS.black : 'white'}
/>
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
Niederspannung
</Text>
</View>
<View style={styles.frontBrandIconWrapper}>
<IconMediumVoltage
style={styles.frontBrandIcon}
color={isWhiteVariant ? COLORS.black : 'white'}
/>
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
Mittelspannung
</Text>
</View>
<View style={styles.frontBrandIconWrapper}>
<IconHighVoltage
style={styles.frontBrandIcon}
color={isWhiteVariant ? COLORS.black : 'white'}
/>
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
Hochspannung
</Text>
</View>
{renderPageWrapper(
<>
<View style={styles.frontFooterArea}>
<Text style={[styles.frontTaglineBar, isWhiteVariant && { color: COLORS.black }]}>
Von 0,6/1,0 kV bis 64/110 kV
</Text>
<View style={styles.frontIconsGroup}>
<View style={styles.frontBrandIconWrapper}>
<IconLowVoltage
style={styles.frontBrandIcon}
color={isWhiteVariant ? COLORS.black : 'white'}
/>
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
Niederspannung
</Text>
</View>
<View style={styles.frontBrandIconWrapper}>
<IconMediumVoltage
style={styles.frontBrandIcon}
color={isWhiteVariant ? COLORS.black : 'white'}
/>
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
Mittelspannung
</Text>
</View>
<View style={styles.frontBrandIconWrapper}>
<IconHighVoltage
style={styles.frontBrandIcon}
color={isWhiteVariant ? COLORS.black : 'white'}
/>
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
Hochspannung
</Text>
</View>
<View style={styles.frontBrandIconWrapper}>
<IconSolar
style={styles.frontBrandIcon}
color={isWhiteVariant ? COLORS.black : 'white'}
/>
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
Solar
</Text>
<View style={styles.frontBrandIconWrapper}>
<IconSolar
style={styles.frontBrandIcon}
color={isWhiteVariant ? COLORS.black : 'white'}
/>
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
Solar
</Text>
</View>
</View>
</View>
</View>
<View style={styles.frontContainer}>
{/* Base Logo: text is white, truck is green. Fill KLZ background to hide truck, keep outline white */}
<VectorLogo
paths={getFrontLogoPaths()}
style={styles.frontLogo}
solidPaths={KLZ_PATHS}
solidBackgroundColor={isWhiteVariant ? '#ffffff' : '#011dff'}
/>
{/* Letterpress Mask Layer: We use #ff00fe as a marker color. Only the "KLZ" letters! */}
{isViaprinto && (
<View style={styles.frontContainer}>
{/* Base Logo: text is white, truck is green. Fill KLZ background to hide truck, keep outline white */}
<VectorLogo
paths={getFrontLogoPaths().filter((_, i) => KLZ_PATHS.includes(i))}
style={[styles.frontLogo, { position: 'absolute' }]}
overrideColor="#ff00fe"
solidPaths={KLZ_PATHS.map((_, i) => i)}
paths={getFrontLogoPaths()}
style={styles.frontLogo}
solidPaths={KLZ_PATHS}
solidBackgroundColor={isWhiteVariant ? '#ffffff' : COLORS.primary}
/>
)}
</View>
</Page>
</View>
</>,
[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 style={styles.addressFooter}>
<Text style={styles.addressFooterText}>
KLZ Vertriebs GmbH · Raiffeisenstraße 22 · 73630 Remshalden
</Text>
</View>
</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,9 +396,11 @@ export const PDFPage: React.FC<PDFPageProps> = ({ page, locale = 'de' }) => {
<View style={styles.content}>
<View>
{page.content?.root?.children?.map((node: any, i: number) =>
renderLexicalNode(node, i),
)}
{typeof page.content === 'string'
? renderMarkdown(page.content)
: page.content?.root?.children?.map((node: any, i: number) =>
renderLexicalNode(node, i),
)}
</View>
</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;
});
});

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;', '>'));
});
});