Syntax Highlighting & Project Filters
Chapter 6
3 min read
To make your portfolio truly professional, you need high-quality code blocks and a way for users to browse your work by category.
We’ll use rehype-pretty-code, which is powered by Shiki—the same engine that powers VS Code. This ensures your code snippets look exactly like they do in your editor.
Installation:
npm install rehype-pretty-code shikiConfiguration (src/app/portfolio/[slug]/page.tsx): Update your MDXRemote options to include the plugin and a VS Code theme (e.g., one-dark-pro).
import rehypePrettyCode from 'rehype-pretty-code';
const options = {
mdxOptions: {
rehypePlugins: [
[
rehypePrettyCode,
{
theme: 'one-dark-pro',
keepBackground: true,
},
],
],
},
};
// Pass to MDXRemote
<MDXRemote source={content} options={options} />For a portfolio, URL-based filtering is superior to state-based filtering because it allows users to share a link to a specific category (e.g., /portfolio?category=frontend).
This is a Client Component that updates the URL search parameters.
"use client";
import { useRouter, useSearchParams } from 'next/navigation';
export default function ProjectFilter({ categories }: { categories: string[] }) {
const router = useRouter();
const searchParams = useSearchParams();
const activeCategory = searchParams.get('category') || 'All';
const setFilter = (cat: string) => {
const params = new URLSearchParams(searchParams.toString());
if (cat === 'All') params.delete('category');
else params.set('category', cat);
router.push(`?${params.toString()}`, { scroll: false });
};
return (
<div className="flex gap-4">
{['All', ...categories].map((cat) => (
<button
key={cat}
onClick={() => setFilter(cat)}
className={activeCategory === cat ? 'active' : ''}
>
{cat}
</button>
))}
</div>
);
}The main page remains a Server Component. It reads the searchParams and filters the data before rendering.
export default async function PortfolioPage({
searchParams
}: {
searchParams: { category?: string }
}) {
const allProjects = await getAllProjects(); // Your helper to fetch MDX
const filtered = searchParams.category
? allProjects.filter(p => p.category === searchParams.category)
: allProjects;
return (
<main>
<ProjectFilter categories={['Frontend', 'Backend', 'Design']} />
<div className="grid">
{filtered.map(project => (
<ProjectCard key={project.slug} {...project} />
))}
</div>
</main>
);
}Why this works:
- SEO: Every filtered view has its own URL.
- Performance: Filtering happens on the server; the client only receives the final HTML.
- No Hydration Flickering: Using searchParams avoids the "jump" often seen with useState filtering.