feat(blog): improve blog overview teasers layout
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 19s
Build & Deploy / 🧪 QA (push) Successful in 5m42s
Build & Deploy / 🏗️ Build (push) Failing after 31s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Smoke Test (push) Has been skipped
Build & Deploy / ⚡ Lighthouse (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 19s
Build & Deploy / 🧪 QA (push) Successful in 5m42s
Build & Deploy / 🏗️ Build (push) Failing after 31s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Smoke Test (push) Has been skipped
Build & Deploy / ⚡ Lighthouse (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
Increases the line clamps for title and excerpt in the blog overview grid. Also parses the markdown content to auto-generate a fallback excerpt if omitted in frontmatter.
This commit is contained in:
@@ -94,7 +94,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
|||||||
<Heading level={1} className="text-white mb-4 md:mb-8">
|
<Heading level={1} className="text-white mb-4 md:mb-8">
|
||||||
{featuredPost.frontmatter.title}
|
{featuredPost.frontmatter.title}
|
||||||
</Heading>
|
</Heading>
|
||||||
<p className="text-base md:text-xl text-white/80 mb-6 md:mb-10 line-clamp-2 md:line-clamp-2 max-w-2xl">
|
<p className="text-base md:text-xl text-white/80 mb-6 md:mb-10 line-clamp-3 md:line-clamp-4 max-w-2xl">
|
||||||
{featuredPost.frontmatter.excerpt}
|
{featuredPost.frontmatter.excerpt}
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
@@ -197,10 +197,10 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg md:text-2xl font-bold text-primary mb-3 md:mb-6 group-hover:text-accent-dark transition-colors line-clamp-2 leading-tight">
|
<h3 className="text-lg md:text-2xl font-bold text-primary mb-3 md:mb-6 group-hover:text-accent-dark transition-colors line-clamp-3 md:line-clamp-4 leading-tight">
|
||||||
{post.frontmatter.title}
|
{post.frontmatter.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-text-secondary text-sm md:text-lg line-clamp-2 md:line-clamp-3 mb-4 md:mb-8 leading-relaxed">
|
<p className="text-text-secondary text-sm md:text-lg line-clamp-3 md:line-clamp-4 mb-4 md:mb-8 leading-relaxed">
|
||||||
{post.frontmatter.excerpt}
|
{post.frontmatter.excerpt}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-auto pt-4 md:pt-8 border-t border-neutral-medium flex items-center justify-between">
|
<div className="mt-auto pt-4 md:pt-8 border-t border-neutral-medium flex items-center justify-between">
|
||||||
|
|||||||
40
lib/blog.ts
40
lib/blog.ts
@@ -4,6 +4,31 @@ import matter from 'gray-matter';
|
|||||||
import { mapSlugToFileSlug } from './slugs';
|
import { mapSlugToFileSlug } from './slugs';
|
||||||
import { config } from '@/lib/config';
|
import { config } from '@/lib/config';
|
||||||
|
|
||||||
|
export function extractExcerpt(content: string): string {
|
||||||
|
if (!content) return '';
|
||||||
|
// Remove frontmatter if present (though matter() usually strips it out)
|
||||||
|
let text = content.replace(/^---[\s\S]*?---/, '');
|
||||||
|
// Remove MDX component imports and usages
|
||||||
|
text = text.replace(/<[^>]+>/g, '');
|
||||||
|
text = text.replace(/^[ \t]*import\s+.*$/gm, '');
|
||||||
|
text = text.replace(/^[ \t]*export\s+.*$/gm, '');
|
||||||
|
// Remove markdown headings
|
||||||
|
text = text.replace(/^#+.*$/gm, '');
|
||||||
|
// Extract first paragraph or combined lines
|
||||||
|
const paragraphs = text
|
||||||
|
.split(/\n\s*\n/)
|
||||||
|
.filter((p) => p.trim() && !p.trim().startsWith('---') && !p.trim().startsWith('#'));
|
||||||
|
if (paragraphs.length === 0) return '';
|
||||||
|
|
||||||
|
const excerpt = paragraphs[0]
|
||||||
|
.replace(/[*_`]/g, '') // remove markdown bold/italic/code
|
||||||
|
.replace(/\[(.*?)\]\(.*?\)/g, '$1') // replace links with their text
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
return excerpt.length > 200 ? excerpt.slice(0, 197) + '...' : excerpt;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PostFrontmatter {
|
export interface PostFrontmatter {
|
||||||
title: string;
|
title: string;
|
||||||
date: string;
|
date: string;
|
||||||
@@ -46,7 +71,10 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostM
|
|||||||
|
|
||||||
const postInfo = {
|
const postInfo = {
|
||||||
slug: fileSlug,
|
slug: fileSlug,
|
||||||
frontmatter: data as PostFrontmatter,
|
frontmatter: {
|
||||||
|
...data,
|
||||||
|
excerpt: data.excerpt || extractExcerpt(content),
|
||||||
|
} as PostFrontmatter,
|
||||||
content,
|
content,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,7 +98,10 @@ export async function getAllPosts(locale: string): Promise<PostMdx[]> {
|
|||||||
const { data, content } = matter(fileContent);
|
const { data, content } = matter(fileContent);
|
||||||
return {
|
return {
|
||||||
slug: file.replace(/\.mdx$/, ''),
|
slug: file.replace(/\.mdx$/, ''),
|
||||||
frontmatter: data as PostFrontmatter,
|
frontmatter: {
|
||||||
|
...data,
|
||||||
|
excerpt: data.excerpt || extractExcerpt(content),
|
||||||
|
} as PostFrontmatter,
|
||||||
content,
|
content,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -95,7 +126,10 @@ export async function getAllPostsMetadata(locale: string): Promise<Partial<PostM
|
|||||||
const { data } = matter(fileContent);
|
const { data } = matter(fileContent);
|
||||||
return {
|
return {
|
||||||
slug: file.replace(/\.mdx$/, ''),
|
slug: file.replace(/\.mdx$/, ''),
|
||||||
frontmatter: data as PostFrontmatter,
|
frontmatter: {
|
||||||
|
...data,
|
||||||
|
excerpt: data.excerpt || extractExcerpt(fileContent.replace(/^---[\s\S]*?---/, '')),
|
||||||
|
} as PostFrontmatter,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(isPostVisible)
|
.filter(isPostVisible)
|
||||||
|
|||||||
Reference in New Issue
Block a user