Building Portfolio in Next.js
Tutorials
Building Portfolio in Next.js

By Subash Maharjan


Chapters
  • Chapter 1

    Introduction

  • Chapter 2

    Project Architecture

  • Chapter 3

    SCSS Design Tokens

  • Chapter 4

    The Hero Component

  • Chapter 5

    The MDX Content Layer

  • Chapter 6

    Syntax Highlighting & Project Filters

  • Chapter 7

    Smooth Layout Transitions

  • Chapter 8

    Modern Contact Forms

  • Chapter 9

    SEO & Deployment

  • Chapter 10

    Lighthouse Performance & Blogging

  • Chapter 11

    Interactive Playgrounds & Subscriptions

  • Chapter 12

    Dynamic Social Previews & Launch

  • Chapter 13

    Project Summary for Recruiters

  • building portfolio in nextjs

    Syntax Highlighting & Project Filters

    Chapter 6

    3 min read

    On this page
    1. Syntax Highlighting with Shiki2. Project Filtering (URL-Based)Step B: The Portfolio Page (page.tsx)
    Company LogoSubash
    HOMEABOUTPORTFOLIOCONTENTSCONTACT

    To make your portfolio truly professional, you need high-quality code blocks and a way for users to browse your work by category.

    1. Syntax Highlighting with Shiki

    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 shiki

    Configuration (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} />

    2. Project Filtering (URL-Based)

    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).

    Step A: The Filter Component (Filter.tsx)

    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>
      );
    }

    Step B: The Portfolio Page (page.tsx)

    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.
    HOME
    ABOUT
    PORTFOLIO
    CONTENTS
    CONTACT

    © 2026Subash Maharjan™

    Made byHudeoworks Design