Compare commits
12 Commits
v2.3.28
...
feat/dynam
| Author | SHA1 | Date | |
|---|---|---|---|
| 60422da644 | |||
| fc6da4e9aa | |||
| f7411d3dc4 | |||
| 7be2ec605c | |||
| bd00175912 | |||
| 9c9663eb88 | |||
| 402b5c30c6 | |||
| 2bb0381de4 | |||
| 614b0a5ffd | |||
| b1569a1ce2 | |||
| 2c35c6b8aa | |||
| bff150b1a7 |
@@ -1,7 +1,7 @@
|
|||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from 'next/og';
|
||||||
import { getPageBySlug } from '@/lib/pages';
|
import { getPageBySlug } from '@/lib/pages';
|
||||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
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 size = OG_IMAGE_SIZE;
|
||||||
export const contentType = 'image/png';
|
export const contentType = 'image/png';
|
||||||
@@ -20,9 +20,12 @@ export default async function Image({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
const { logo, bgImage } = getOgAssets();
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
<OGImageTemplate
|
<OGImageTemplate
|
||||||
|
image={bgImage}
|
||||||
|
logo={logo}
|
||||||
title={pageData.frontmatter.title}
|
title={pageData.frontmatter.title}
|
||||||
description={pageData.frontmatter.excerpt}
|
description={pageData.frontmatter.excerpt}
|
||||||
label="Information"
|
label="Information"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from 'next/og';
|
||||||
import { getPostBySlug } from '@/lib/blog';
|
import { getPostBySlug } from '@/lib/blog';
|
||||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
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';
|
import { SITE_URL } from '@/lib/schema';
|
||||||
|
|
||||||
export const size = OG_IMAGE_SIZE;
|
export const size = OG_IMAGE_SIZE;
|
||||||
@@ -35,6 +35,7 @@ export default async function Image({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fonts = await getOgFonts();
|
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
|
// 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
|
// 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(
|
return new ImageResponse(
|
||||||
<OGImageTemplate
|
<OGImageTemplate
|
||||||
|
logo={logo}
|
||||||
title={post.frontmatter.title}
|
title={post.frontmatter.title}
|
||||||
description={post.frontmatter.excerpt}
|
description={post.frontmatter.excerpt}
|
||||||
label={post.frontmatter.category || 'Blog'}
|
label={post.frontmatter.category || 'Blog'}
|
||||||
image={base64Image || featuredImage}
|
image={base64Image || featuredImage || bgImage}
|
||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
...OG_IMAGE_SIZE,
|
...OG_IMAGE_SIZE,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from 'next/og';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
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 size = OG_IMAGE_SIZE;
|
||||||
export const contentType = 'image/png';
|
export const contentType = 'image/png';
|
||||||
@@ -11,9 +11,16 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
|||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
const t = await getTranslations({ locale, namespace: 'Blog.meta' });
|
const t = await getTranslations({ locale, namespace: 'Blog.meta' });
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
const { logo, bgImage } = getOgAssets();
|
||||||
|
|
||||||
return new ImageResponse(
|
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,
|
...OG_IMAGE_SIZE,
|
||||||
fonts,
|
fonts,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from 'next/og';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
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 size = OG_IMAGE_SIZE;
|
||||||
export const contentType = 'image/png';
|
export const contentType = 'image/png';
|
||||||
@@ -11,12 +11,19 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
|||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
const t = await getTranslations({ locale, namespace: 'Contact' });
|
const t = await getTranslations({ locale, namespace: 'Contact' });
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
const { logo, bgImage } = getOgAssets();
|
||||||
|
|
||||||
const title = t('meta.title') || t('title');
|
const title = t('meta.title') || t('title');
|
||||||
const description = t('meta.description') || t('subtitle');
|
const description = t('meta.description') || t('subtitle');
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
<OGImageTemplate title={title} description={description} label="Contact" />,
|
<OGImageTemplate
|
||||||
|
image={bgImage}
|
||||||
|
logo={logo}
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
label="Contact"
|
||||||
|
/>,
|
||||||
{
|
{
|
||||||
...OG_IMAGE_SIZE,
|
...OG_IMAGE_SIZE,
|
||||||
fonts,
|
fonts,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from 'next/og';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
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 size = OG_IMAGE_SIZE;
|
||||||
export const contentType = 'image/png';
|
export const contentType = 'image/png';
|
||||||
@@ -11,9 +11,12 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
|||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
const t = await getTranslations({ locale, namespace: 'Index.meta' });
|
const t = await getTranslations({ locale, namespace: 'Index.meta' });
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
const { logo, bgImage } = getOgAssets();
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
<OGImageTemplate
|
<OGImageTemplate
|
||||||
|
image={bgImage}
|
||||||
|
logo={logo}
|
||||||
title={t('title')}
|
title={t('title')}
|
||||||
description={t('description')}
|
description={t('description')}
|
||||||
label="Reliable Energy Infrastructure"
|
label="Reliable Energy Infrastructure"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from 'next/og';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
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 size = OG_IMAGE_SIZE;
|
||||||
export const contentType = 'image/png';
|
export const contentType = 'image/png';
|
||||||
@@ -11,6 +11,7 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
|||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
const t = await getTranslations({ locale, namespace: 'Products' });
|
const t = await getTranslations({ locale, namespace: 'Products' });
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
const { logo, bgImage } = getOgAssets();
|
||||||
|
|
||||||
const title = t.has('meta.title')
|
const title = t.has('meta.title')
|
||||||
? t('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');
|
const description = t('meta.description') || t('subtitle');
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
<OGImageTemplate title={title} description={description} label="Products" />,
|
<OGImageTemplate
|
||||||
|
image={bgImage}
|
||||||
|
logo={logo}
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
label="Products"
|
||||||
|
/>,
|
||||||
{
|
{
|
||||||
...OG_IMAGE_SIZE,
|
...OG_IMAGE_SIZE,
|
||||||
fonts,
|
fonts,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from 'next/og';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
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 size = OG_IMAGE_SIZE;
|
||||||
export const contentType = 'image/png';
|
export const contentType = 'image/png';
|
||||||
@@ -11,12 +11,19 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
|||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
const t = await getTranslations({ locale, namespace: 'Team' });
|
const t = await getTranslations({ locale, namespace: 'Team' });
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
const { logo, bgImage } = getOgAssets();
|
||||||
|
|
||||||
const title = t('meta.title') || t('hero.subtitle');
|
const title = t('meta.title') || t('hero.subtitle');
|
||||||
const description = t('meta.description') || t('hero.title');
|
const description = t('meta.description') || t('hero.title');
|
||||||
|
|
||||||
return new ImageResponse(
|
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,
|
...OG_IMAGE_SIZE,
|
||||||
fonts,
|
fonts,
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
productName,
|
productName,
|
||||||
message,
|
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 });
|
logger.info('Successfully saved lead to local backup file', { backupFile });
|
||||||
} catch (backupError) {
|
} catch (backupError) {
|
||||||
logger.error('Failed to write to local leads backup', { error: String(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)
|
// Notify via Gotify (Internal)
|
||||||
await services.notifications.notify({
|
await services.notifications.notify({
|
||||||
title: `📩 ${notificationSubject}`,
|
title: `📩 [KLZ] ${notificationSubject}`,
|
||||||
message: `New message from ${name} (${email}):\n\n${message}`,
|
message: `New message from ${name} (${email}):\n\n${message}`,
|
||||||
priority: 5,
|
priority: 5,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ interface OGImageTemplateProps {
|
|||||||
description?: string;
|
description?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
|
logo?: string;
|
||||||
mode?: 'dark' | 'light' | 'image';
|
mode?: 'dark' | 'light' | 'image';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ export function OGImageTemplate({
|
|||||||
description,
|
description,
|
||||||
label,
|
label,
|
||||||
image,
|
image,
|
||||||
|
logo,
|
||||||
mode = 'dark',
|
mode = 'dark',
|
||||||
}: OGImageTemplateProps) {
|
}: OGImageTemplateProps) {
|
||||||
const primaryBlue = '#001a4d';
|
const primaryBlue = '#001a4d';
|
||||||
@@ -152,21 +154,26 @@ export function OGImageTemplate({
|
|||||||
height: '6px',
|
height: '6px',
|
||||||
backgroundColor: accentGreen,
|
backgroundColor: accentGreen,
|
||||||
borderRadius: '3px',
|
borderRadius: '3px',
|
||||||
marginRight: '24px',
|
marginRight: '32px',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
{logo ? (
|
||||||
style={{
|
/* eslint-disable-next-line @next/next/no-img-element */
|
||||||
fontSize: '24px',
|
<img src={logo} alt="KLZ Cables" height="40" style={{ objectFit: 'contain' }} />
|
||||||
fontWeight: 700,
|
) : (
|
||||||
color: 'white',
|
<div
|
||||||
textTransform: 'uppercase',
|
style={{
|
||||||
letterSpacing: '0.15em',
|
fontSize: '24px',
|
||||||
display: 'flex',
|
fontWeight: 700,
|
||||||
}}
|
color: 'white',
|
||||||
>
|
textTransform: 'uppercase',
|
||||||
KLZ Cables
|
letterSpacing: '0.15em',
|
||||||
</div>
|
display: 'flex',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
KLZ Cables
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Saturated Blue Brand Strip */}
|
{/* Saturated Blue Brand Strip */}
|
||||||
|
|||||||
@@ -65,11 +65,8 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostD
|
|||||||
const fileContent = await fs.readFile(filePath, 'utf-8');
|
const fileContent = await fs.readFile(filePath, 'utf-8');
|
||||||
const { data, content } = matter(fileContent);
|
const { data, content } = matter(fileContent);
|
||||||
|
|
||||||
// Fix MDX data props dropped by next-mdx-remote
|
const { fixMdxDataProps } = await import('./mdx-utils');
|
||||||
// Payload serializes as data={{"items":...}} which Acorn treats as a Block statement.
|
const fixedContent = fixMdxDataProps(content);
|
||||||
const fixedContent = content.replace(/data=\{\{([\s\S]*?)\}\}/g, (match, p1) => {
|
|
||||||
return 'data="{' + p1.replace(/"/g, '"') + '}"';
|
|
||||||
});
|
|
||||||
|
|
||||||
let parsedContent = fixedContent;
|
let parsedContent = fixedContent;
|
||||||
try {
|
try {
|
||||||
|
|||||||
68
lib/mdx-utils.ts
Normal file
68
lib/mdx-utils.ts
Normal 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, '"');
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -55,3 +55,36 @@ export const OG_IMAGE_SIZE = {
|
|||||||
width: 1200,
|
width: 1200,
|
||||||
height: 630,
|
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',
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,11 +53,8 @@ export async function getPageBySlug(slug: string, locale: string): Promise<PageD
|
|||||||
const fileContent = await fs.readFile(filePath, 'utf-8');
|
const fileContent = await fs.readFile(filePath, 'utf-8');
|
||||||
const { data, content } = matter(fileContent);
|
const { data, content } = matter(fileContent);
|
||||||
|
|
||||||
// Fix MDX data props dropped by next-mdx-remote
|
const { fixMdxDataProps } = await import('./mdx-utils');
|
||||||
// Payload serializes as data={{"items":...}} which Acorn treats as a Block statement.
|
const fixedContent = fixMdxDataProps(content);
|
||||||
const fixedContent = content.replace(/data=\{\{([\s\S]*?)\}\}/g, (match, p1) => {
|
|
||||||
return 'data="{' + p1.replace(/"/g, '"') + '}"';
|
|
||||||
});
|
|
||||||
|
|
||||||
let parsedContent = fixedContent;
|
let parsedContent = fixedContent;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -39,11 +39,8 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
|
|||||||
const fileContent = await fs.readFile(filePath, 'utf-8');
|
const fileContent = await fs.readFile(filePath, 'utf-8');
|
||||||
const { data, content } = matter(fileContent);
|
const { data, content } = matter(fileContent);
|
||||||
|
|
||||||
// Fix MDX data props dropped by next-mdx-remote
|
const { fixMdxDataProps } = await import('./mdx-utils');
|
||||||
// Payload serializes as data={{"items":...}} which Acorn treats as a Block statement.
|
const fixedContent = fixMdxDataProps(content);
|
||||||
const fixedContent = content.replace(/data=\{\{([\s\S]*?)\}\}/g, (match, p1) => {
|
|
||||||
return 'data="{' + p1.replace(/"/g, '"') + '}"';
|
|
||||||
});
|
|
||||||
|
|
||||||
let parsedContent = fixedContent;
|
let parsedContent = fixedContent;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ export class GlitchtipErrorReportingService implements ErrorReportingService {
|
|||||||
tracesSampleRate: this.options.tracesSampleRate ?? 0.1,
|
tracesSampleRate: this.options.tracesSampleRate ?? 0.1,
|
||||||
replaysOnErrorSampleRate: 1.0,
|
replaysOnErrorSampleRate: 1.0,
|
||||||
replaysSessionSampleRate: 0.1,
|
replaysSessionSampleRate: 0.1,
|
||||||
|
ignoreErrors: [
|
||||||
|
'ChunkLoadError',
|
||||||
|
'Failed to fetch dynamically imported module',
|
||||||
|
'Failed to find Server Action',
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Sentry;
|
return Sentry;
|
||||||
|
|||||||
@@ -116,7 +116,7 @@
|
|||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"preinstall": "npx only-allow pnpm"
|
"preinstall": "npx only-allow pnpm"
|
||||||
},
|
},
|
||||||
"version": "2.3.28",
|
"version": "2.3.32",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"@parcel/watcher",
|
"@parcel/watcher",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -12,4 +12,5 @@ Sentry.init({
|
|||||||
|
|
||||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
debug: false,
|
debug: false,
|
||||||
|
ignoreErrors: ['failed to pipe response'],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ Sentry.init({
|
|||||||
|
|
||||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
debug: false,
|
debug: false,
|
||||||
|
ignoreErrors: ['failed to pipe response'],
|
||||||
});
|
});
|
||||||
|
|||||||
47
tests/glitchtip-error-reporting.test.ts
Normal file
47
tests/glitchtip-error-reporting.test.ts
Normal 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
22
tests/mdx-regex.test.ts
Normal 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="{"content":{"root":{"children":[]}},"id":"123"}" />`;
|
||||||
|
|
||||||
|
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="{"content":{"text":"}>"}}" />`;
|
||||||
|
expect(result).toBe(expected.replace('>', '>'));
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user