Skip to main content

Command Palette

Search for a command to run...

18 Redesigning My Portfolio Website

Breaking Down Information Silos with ClickRise

Updated
8 min read
18 Redesigning My Portfolio Website

Published: February 10, 2026

As my GitHub account crept toward 149 repositories and my active coding projects crossed the 50-project mark, I hit a wall that every prolific developer eventually faces: information silos. Project statuses lived in GitHub, descriptions in a local JSON file, deployment URLs in Vercel, and screenshots in Cloudinary. Updating a single project meant touching four different systems. Something had to change.

This post covers the most significant architectural shift my portfolio has undergone since its inception — migrating from static, hand-maintained project data to a live integration with ClickRise, my own project management platform, and replacing manual screenshots with Microlink-powered live previews.

The Information Silo Problem

Before this migration, every project in my portfolio lived in a single TypeScript file — projectData.ts — that had ballooned to over 400 lines. Adding a new project meant:

  1. Manually writing a ProjectData object with name, description, technologies, dates, and URLs

  2. Taking a screenshot, uploading it to Cloudinary, and pasting the URL

  3. Keeping the status and progress in sync with what was actually happening in development

  4. Remembering to update endDate whenever I pushed a commit (I even had a GitHub Action for this)

With close to 50 projects, this was unsustainable. Data went stale within days. Projects I had deployed weeks ago still showed "scaffolded" status. Screenshots captured months prior no longer reflected the current UI.

Enter ClickRise: A Single Source of Truth

ClickRise is a project management platform I built specifically for tracking development work across multiple workspaces. The key insight was: if ClickRise already knows a project's status, progress, deployment URL, technologies, and team size — why duplicate all of that in my portfolio?

The integration meant building three layers:

Layer 1: Type System (lib/clickrise.ts)

First, I defined a comprehensive type system that maps ClickRise's project model to my portfolio's needs:

export interface PortfolioProject extends ClickRiseProjectExtended {
  portfolio: {
    isFeatured: boolean
    displayOrder: number
    isPublic: boolean
    slug: string
    coverImage: CoverImageConfig
    screenshots: PortfolioScreenshot[]
    meta: {
      title: string | null
      description: string | null
    }
  }
}

This extended interface augments ClickRise's core project data with portfolio-specific metadata like featured status, display order, and cover image configuration. The separation is intentional — ClickRise manages the development reality (status, progress, tasks), while the portfolio layer controls the presentation.

Layer 2: Server-Side API Client (lib/clickrise-client.ts)

The API client handles fetching raw project data from ClickRise and transforming it into the PortfolioProject format. The critical function is transformToPortfolioProject():

function transformToPortfolioProject(raw: RawClickRiseProject): PortfolioProject {
  const slug = raw.slug || generateSlug(raw.name)
  const liveUrl = raw.href || raw.liveUrl || raw.deploymentUrl

  // Determine cover image with smart fallback chain
  let coverImageUrl = (raw.landing_image_url || raw.image)?.trim() || null
  let coverImageSource: 'manual' | 'microlink' | 'cloudinary' = 'manual'

  if (coverImageUrl && coverImageUrl.includes('cloudinary')) {
    coverImageSource = 'cloudinary'
  }

  // Use Microlink screenshot for any project with a live URL but no image
  if (!coverImageUrl && liveUrl) {
    coverImageUrl = getMicrolinkScreenshotUrl(liveUrl)
    coverImageSource = 'microlink'
  } else if (!coverImageUrl) {
    coverImageUrl = '/assets/coming-soon.png'
  }

  return {
    id: raw.id,
    name: raw.name,
    status: raw.status,
    devProgress: raw.devProgress,
    deploymentUrl: liveUrl,
    technologies: raw.technologies ?? [],
    portfolio: {
      slug,
      coverImage: { url: coverImageUrl, source: coverImageSource, fallback: '/assets/coming-soon.png' },
      // ... rest of portfolio metadata
    },
  } as PortfolioProject
}

This transformation layer is where the magic happens. It resolves image sources through a priority chain (manual upload > Cloudinary > Microlink > placeholder), parses technology arrays, and merges development metadata with portfolio display settings.

Layer 3: API Proxy Route (app/api/clickrise/projects/route.ts)

To keep the ClickRise API key secure, the portfolio fetches data through a local API proxy:

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url)
  const featured = searchParams.get('featured') === 'true'

  let projects
  if (featured) {
    projects = await fetchFeaturedProjectsData(limit)
  } else {
    projects = await fetchProjects({
      status: searchParams.get('status'),
      devProgress: searchParams.get('devProgress'),
      sortBy: searchParams.get('sortBy'),
      // ...
    })
  }

  return NextResponse.json({
    success: true,
    data: { projects },
    meta: { count: projects.length, source: 'clickrise', timestamp: new Date().toISOString() }
  })
}

The client-side hooks consume this proxy, not the ClickRise API directly. This means API keys never leave the server, and I get a natural caching boundary.

Workspace Filtering: Separating Coding Projects from Everything Else

ClickRise manages more than just coding projects — it tracks personal goals, writing projects, and other workspaces. The portfolio should only show coding work, so I added workspace-based filtering:

export const PORTFOLIO_WORKSPACE_ID =
  process.env.CLICKRISE_CODING_WORKSPACE_ID || null

export async function fetchPortfolioProjects(query?: PortfolioProjectsQuery) {
  const client = getClickRiseClient()
  const queryWithWorkspace = {
    workspaceId: PORTFOLIO_WORKSPACE_ID,
    ...query,
  }
  const response = await client.getProjects(queryWithWorkspace)
  return response.data.projects
}

This single filter reduced the portfolio from showing every ClickRise project to only the 50+ coding projects in the "Coding Projects" workspace.

The second major shift was eliminating manually captured screenshots entirely. Previously, I maintained a Cloudinary folder of screenshots that I captured one by one by a rudimentary Python script. They went stale almost immediately — I'd ship a dark mode redesign and the portfolio would still show the old light-mode UI for weeks. The Python script was previously setup as a GitHub workflow to capture new screenshots weekly and upload them to Cloudinary, but that was short-lived and stopped working.

Microlink provides an API that takes any URL and returns a live screenshot. Instead of storing images, the portfolio now generates screenshot URLs dynamically:

export function getMicrolinkScreenshotUrl(
  deploymentUrl: string,
  options: Partial<MicrolinkScreenshotOptions> = {}
): string {
  const params = new URLSearchParams()
  params.set('url', deploymentUrl)
  params.set('screenshot', 'true')
  params.set('meta', 'false')
  params.set('embed', 'screenshot.url')
  params.set('screenshot.delay', String(options.screenshotDelay ?? 4000))
  params.set('waitForTimeout', String(options.waitForTimeout ?? 5000))
  params.set('waitUntil', options.waitUntil ?? 'networkidle0')

  if (options.colorScheme && options.colorScheme !== 'no-preference') {
    params.set('colorScheme', options.colorScheme)
  }

  return `https://api.microlink.io/?${params.toString()}`
}

The screenshot.delay of 4 seconds is critical — it gives JavaScript-heavy Next.js apps time to hydrate and render before the screenshot is captured. Without it, you get blank pages or loading spinners or worse, a blurry mess.

Alternating Dark/Light Themes

To add visual variety to the portfolio grid, I alternate screenshot themes based on card index:

// In ProjectCard.tsx
const screenshotTheme = index % 2 === 0 ? 'light' : 'dark'
const cardImage = image && !image.includes('coming-soon')
  ? image
  : deploymentUrl
    ? getScreenshotProxyUrl(deploymentUrl, { colorScheme: screenshotTheme })
    : image || "https://picsum.photos/400/250"

Even-indexed cards get light-mode screenshots, odd-indexed cards get dark-mode. This creates a natural visual rhythm in the grid without any manual effort.

Screenshot Proxy with Edge Caching

On Microlink's free tier, aggressive caching is essential. I built a screenshot proxy route that caches responses on Vercel's CDN:

// app/api/screenshot/route.ts
const TWENTY_FOUR_HOURS = 86400

export async function GET(request: NextRequest) {
  const url = request.nextUrl.searchParams.get('url')
  const colorScheme = request.nextUrl.searchParams.get('colorScheme') || 'light'

  const response = await fetch(
    `https://api.microlink.io/?${params.toString()}`,
    { next: { revalidate: TWENTY_FOUR_HOURS, tags: ['screenshots'] } }
  )

  const imageBuffer = await response.arrayBuffer()

  return new NextResponse(imageBuffer, {
    headers: {
      'Cache-Control': `public, s-maxage=${TWENTY_FOUR_HOURS}, stale-while-revalidate=3600`,
      'CDN-Cache-Control': `public, s-maxage=${TWENTY_FOUR_HOURS}`,
      'Vercel-CDN-Cache-Control': `public, s-maxage=${TWENTY_FOUR_HOURS}`,
    },
  })
}

Screenshots are cached for 24 hours at the CDN edge, with a 1-hour stale-while-revalidate window. This means Microlink is called at most once per day per project, keeping well within free tier limits.

Dev Progress Tracking: Showing the Full Journey

With ClickRise feeding live data, I could surface much richer information than just "here's a project." The portfolio now shows:

  • Dev Progress: Not Started > Scaffolded > MVP > Deployed > Archived

  • Task Progress: Completion bars showing tasks done vs. total (e.g., 6/15 tasks)

  • Project Status: Planning, Active, Completed, or Archived

  • Live Dates: Start and end dates synced from ClickRise, with duration calculations

const DEV_PROGRESS_CONFIG: Record<DevProgress, { label: string; color: string; icon: ReactNode }> = {
  not_started: { label: 'Not Started', color: 'bg-gray-100 ...', icon: <Circle /> },
  scaffolded:  { label: 'Scaffolded',  color: 'bg-orange-100 ...', icon: <Hammer /> },
  mvp:         { label: 'MVP',         color: 'bg-purple-100 ...', icon: <Rocket /> },
  deployed:    { label: 'Deployed',    color: 'bg-green-100 ...', icon: <Globe /> },
  archived:    { label: 'Archived',    color: 'bg-gray-100 ...', icon: <Archive /> },
}

These badges appear directly on the project card image overlay, giving visitors an instant read on where each project stands.

The Numbers Tell the Story

After the migration, here's where my portfolio stands:

MetricBefore (Static)After (ClickRise)
Projects displayed~25 (manually maintained)52 (auto-synced)
Data freshnessDays/weeks staleReal-time
ScreenshotsManual Cloudinary uploadsLive Microlink captures
Adding a new projectEdit TypeScript + upload imageJust add to ClickRise
Technologies tracked~20106 unique technologies
Code for project data400+ line TypeScript file0 lines (API-driven)

And across my broader development ecosystem:

  • 149 GitHub repositories and counting

  • 52 active coding projects tracked in ClickRise

  • 25 deployed applications with live screenshots

  • 335 commits to the portfolio repo in the past year

  • 106 unique technologies across all projects

Other Notable Changes

Beyond the ClickRise integration, the portfolio saw several other improvements:

  • Next.js 16 Upgrade: Migrated from Next.js 15 to 16, picking up Turbopack improvements and React 19 features

  • Bun Migration: Switched from npm to bun as the package manager for faster installs and builds

  • Semantic Versioning: Added automated semantic-release for consistent versioning (currently at v1.10.3)

  • Security Hardening: Addressed React2Shell CVE-2025-66478 and other vulnerability fixes

  • Skeleton Loading States: Added proper loading skeletons instead of blank screens while ClickRise data loads

  • Featured Project Ordering: Featured projects on the landing page now respect displayOrder from ClickRise

What's Next

The ClickRise integration opened doors I didn't anticipate:

  • AI-powered project summaries: Using ClickRise task data to auto-generate project descriptions

  • Commit activity heatmaps: Pulling GitHub commit data per project for activity visualization

  • Cross-project analytics: Understanding which technologies I use most and how my skills have evolved

  • Automated freshness checks: Flagging projects whose screenshots are older than their last deployment

The biggest lesson from this migration: the best portfolio is one that maintains itself. By making ClickRise the single source of truth, I freed up hours that used to go into data entry and spent them on what actually matters — building things.


This is Part 18 of my ongoing portfolio redesign series. Read Part 17 for the previous installment covering the gratitude section and storytelling updates.

Redesign Portfolio

Part 1 of 18

In this series, I will post about my progress in redesigning my entire portfolio website until the site is live

Up next

🎨 17 Redesigning My Portfolio Website

A Month of Meaningful Transformations