diff --git a/components/blog/MDXComponents.tsx b/components/blog/MDXComponents.tsx
index 11fcdd8f..f4a68ebc 100644
--- a/components/blog/MDXComponents.tsx
+++ b/components/blog/MDXComponents.tsx
@@ -10,6 +10,7 @@ import PowerCTA from '@/components/blog/PowerCTA';
import StickyNarrative from '@/components/blog/StickyNarrative';
import TechnicalGrid from '@/components/blog/TechnicalGrid';
import ComparisonGrid from '@/components/blog/ComparisonGrid';
+import { generateHeadingId, getTextContent } from '@/lib/blog';
export const mdxComponents = {
VisualLinkPreview,
@@ -36,17 +37,28 @@ export const mdxComponents = {
className="inline-flex items-center gap-3 px-6 py-3 bg-primary text-white font-bold rounded-xl hover:bg-accent hover:text-primary-dark transition-all duration-300 no-underline my-8 group shadow-lg hover:shadow-xl hover:-translate-y-1"
>
{children}
- (PDF)
+
+ (PDF)
+
);
}
if (href?.startsWith('/')) {
return (
-
+
{children}
);
@@ -61,18 +73,19 @@ export const mdxComponents = {
>
{children}
);
},
- img: (props: any) => (
-
- ),
+ img: (props: any) => ,
h2: ({ children, ...props }: any) => {
- const id = typeof children === 'string'
- ? children.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-')
- : props.id;
+ const id = props.id || generateHeadingId(getTextContent(children));
return (
{children}
@@ -80,9 +93,7 @@ export const mdxComponents = {
);
},
h3: ({ children, ...props }: any) => {
- const id = typeof children === 'string'
- ? children.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-')
- : props.id;
+ const id = props.id || generateHeadingId(getTextContent(children));
return (
{children}
@@ -108,17 +119,22 @@ export const mdxComponents = {
{children}
),
blockquote: ({ children, ...props }: any) => (
-
-
- {children}
-
+
+ {children}
),
strong: ({ children, ...props }: any) => (
@@ -144,7 +160,10 @@ export const mdxComponents = {
),
thead: ({ children, ...props }: any) => (
-
+
{children}
),
diff --git a/lib/blog.ts b/lib/blog.ts
index 8a15bbac..0b301f2c 100644
--- a/lib/blog.ts
+++ b/lib/blog.ts
@@ -109,7 +109,12 @@ export async function getAllPostsMetadata(locale: string): Promise {
+): Promise<{
+ prev: PostMdx | null;
+ next: PostMdx | null;
+ isPrevRandom?: boolean;
+ isNextRandom?: boolean;
+}> {
const posts = await getAllPosts(locale);
const currentIndex = posts.findIndex((post) => post.slug === slug);
@@ -127,7 +132,7 @@ export async function getAdjacentPosts(
let isPrevRandom = false;
const getRandomPost = (excludeSlugs: string[]) => {
- const available = posts.filter(p => !excludeSlugs.includes(p.slug));
+ const available = posts.filter((p) => !excludeSlugs.includes(p.slug));
if (available.length === 0) return null;
return available[Math.floor(Math.random() * available.length)];
};
@@ -154,17 +159,42 @@ export function getReadingTime(content: string): number {
return Math.ceil(minutes);
}
+export function generateHeadingId(text: string): string {
+ let id = text.toLowerCase();
+ id = id.replace(/ä/g, 'ae');
+ id = id.replace(/ö/g, 'oe');
+ id = id.replace(/ü/g, 'ue');
+ id = id.replace(/ß/g, 'ss');
+
+ id = id.replace(/[*_`]/g, '');
+ id = id.replace(/[^\w\s-]/g, '');
+ id = id
+ .replace(/\s+/g, '-')
+ .replace(/-+/g, '-')
+ .replace(/^-+|-+$/g, '');
+
+ return id || 'heading';
+}
+
+export function getTextContent(node: any): string {
+ if (typeof node === 'string') return node;
+ if (typeof node === 'number') return node.toString();
+ if (Array.isArray(node)) return node.map(getTextContent).join('');
+ if (node && typeof node === 'object' && node.props && node.props.children) {
+ return getTextContent(node.props.children);
+ }
+ return '';
+}
+
export function getHeadings(content: string): { id: string; text: string; level: number }[] {
const headingLines = content.split('\n').filter((line) => line.match(/^#{2,3}\s/));
return headingLines.map((line) => {
const level = line.match(/^#+/)?.[0].length || 0;
- const text = line.replace(/^#+\s/, '').trim();
- const id = text
- .toLowerCase()
- .replace(/[^\w\s-]/g, '')
- .replace(/\s+/g, '-');
+ const rawText = line.replace(/^#+\s/, '').trim();
+ const cleanText = rawText.replace(/[*_`]/g, '');
+ const id = generateHeadingId(cleanText);
- return { id, text, level };
+ return { id, text: cleanText, level };
});
}