18 Redesigning My Portfolio Website
Breaking Down Information Silos with ClickRise

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:
Manually writing a
ProjectDataobject with name, description, technologies, dates, and URLsTaking a screenshot, uploading it to Cloudinary, and pasting the URL
Keeping the status and progress in sync with what was actually happening in development
Remembering to update
endDatewhenever 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.
Killing Static Screenshots with Microlink
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.
How Microlink Works
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:
| Metric | Before (Static) | After (ClickRise) |
| Projects displayed | ~25 (manually maintained) | 52 (auto-synced) |
| Data freshness | Days/weeks stale | Real-time |
| Screenshots | Manual Cloudinary uploads | Live Microlink captures |
| Adding a new project | Edit TypeScript + upload image | Just add to ClickRise |
| Technologies tracked | ~20 | 106 unique technologies |
| Code for project data | 400+ line TypeScript file | 0 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
displayOrderfrom 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.






