Compare commits

...

9 Commits

Author SHA1 Message Date
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
18 changed files with 175 additions and 43 deletions

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

@@ -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

@@ -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

@@ -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 {

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

@@ -0,0 +1,46 @@
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={{'
for (let i = startObj; i < fixedContent.length; i++) {
if (fixedContent[i] === '{') {
openCount++;
} else if (fixedContent[i] === '}') {
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

@@ -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

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

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

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

@@ -0,0 +1,17 @@
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);
// The expected output should have the entire JSON object enclosed in data="{...}"
// and NO trailing characters left over from the regex truncating early.
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);
});
});