DEV Community

Cover image for Case Studies, Not Screenshots: Freelance Portfolio Pieces Clients Actually Read (Template + Playwright Thumbnails)
Abid Ali
Abid Ali

Posted on

Case Studies, Not Screenshots: Freelance Portfolio Pieces Clients Actually Read (Template + Playwright Thumbnails)

Most developer portfolios on freelance platforms look the same: six screenshots of hero sections, each titled with a client's brand name, each with a description like "Developed a responsive website using React". A client hiring for a five-page business site scrolls through them and learns almost nothing. Was it fast? Did it ship on time? What did you do, and what did a designer or another developer do?

Clients don't read portfolios as galleries. They read them as evidence. One item that clearly matches their job beats six pretty tiles that don't.

This post is the system I'd suggest to any web developer setting up a freelance profile: pick the right projects, build a public demo when you can't show the real thing, write each item as a short case study, and generate thumbnails and numbers with a script so every item looks consistent and is honest.

1. Choose items that match the job you want

Before writing anything, write your offer in one line. For example: "Fast Next.js marketing sites for small businesses." Now go through your past work and keep only what supports that line.

A rough filter:

  • Keep: projects that are the same kind of deliverable as your offer, that you can link to or demo, and where you can clearly state your role.
  • Rewrite: good projects with weak descriptions.
  • Drop: coursework from unrelated stacks, half-finished clones, anything you can't explain in two sentences.

Three items that match your offer are enough to start. More items that don't match make the profile look unfocused.

2. When the real work is under NDA, build a demo

A lot of good freelance work can't be shown: agency white-label projects, internal tools, clients who simply said no. Don't upload screenshots you're not allowed to share, and don't pass off someone else's design as yours. Build a small public demo instead.

Good demo projects are small, real and boring in the right way:

  • A landing page with a working contact form, validation, spam protection and a thank-you state.
  • A five-page site for a fictional business with a real content structure, a blog index and a 404 page.
  • A dashboard screen with loading, empty and error states.

Put the code in a public repo, deploy it to a stable URL, and label it honestly: "Demo project built to show my approach to small business sites." A live link a client can click on their phone is stronger evidence than any screenshot.

3. Write each item as a five-part case study

I keep one Markdown file per portfolio item in a portfolio/ folder. The structure is always the same:

---
title: "Five-page site for a local dental clinic (demo)"
type: business-site
stack: [Next.js, Tailwind, Formspree]
role: "Design adaptation, build, deployment"
live: https://demo-clinic.example.com
repo: https://github.com/you/demo-clinic
---

## The problem
One or two sentences about what the client (or the fictional client) needed.

## Constraints
Budget, deadline, existing brand, who wrote the copy, hosting limits.

## What I did
3 to 5 bullets. Use "I" for your work and name anything you didn't do.

## Result
Facts you can check: shipped on schedule, Lighthouse scores, page weight,
"client approved in one revision round". No invented percentages.

## What I'd do differently
One honest sentence. Clients trust people who can review their own work.
Enter fullscreen mode Exit fullscreen mode

The "Constraints" and "What I'd do differently" sections are the ones most portfolios skip, and they're what makes a client think "this person has done real projects".

When you paste into the platform's portfolio form, the short description usually becomes the problem plus one line of result. The full case study can live on your own site or in the repo README, linked from the item.

4. Get real numbers with Lighthouse

"Fast and responsive" means nothing. A Lighthouse score from a reproducible run means something, as long as you report it honestly (mobile, throttled, on the live URL).

Lighthouse has a CLI that outputs JSON:

npx lighthouse https://demo-clinic.example.com \
  --only-categories=performance,accessibility,best-practices,seo \
  --form-factor=mobile \
  --output=json --output-path=./reports/demo-clinic.json \
  --chrome-flags="--headless=new"
Enter fullscreen mode Exit fullscreen mode

A tiny Node script pulls the scores out so you can paste them into the case study:

// scripts/scores.mjs
import { readFileSync } from "node:fs";

const file = process.argv[2];
const report = JSON.parse(readFileSync(file, "utf8"));

const scores = Object.fromEntries(
  Object.entries(report.categories).map(([key, c]) => [key, Math.round(c.score * 100)])
);

console.log(`Lighthouse (mobile), ${new Date(report.fetchTime).toISOString().slice(0, 10)}:`, scores);
Enter fullscreen mode Exit fullscreen mode
node scripts/scores.mjs reports/demo-clinic.json
Enter fullscreen mode Exit fullscreen mode

Two rules for honesty: put the date next to the score, and rerun it if you change the site. Scores vary between runs, so don't cherry-pick the best of ten.

5. Consistent thumbnails with Playwright

Thumbnails are the first thing a client sees. Mismatched screenshots (one cropped on a laptop, one from a phone, one with browser chrome) make a profile look careless. A script makes them identical:

// scripts/thumbs.mjs
import { chromium, devices } from "playwright";
import { mkdir } from "node:fs/promises";

const items = [
  { slug: "demo-clinic", url: "https://demo-clinic.example.com" },
  { slug: "landing-form", url: "https://landing-demo.example.com" },
];

await mkdir("thumbs", { recursive: true });
const browser = await chromium.launch();

for (const { slug, url } of items) {
  // Desktop: 1600x1000 viewport, top of the page only
  const desktop = await browser.newPage({ viewport: { width: 1600, height: 1000 } });
  await desktop.goto(url, { waitUntil: "networkidle" });
  await desktop.screenshot({ path: `thumbs/${slug}-desktop.png` });
  await desktop.close();

  // Mobile: proves it's responsive without saying "responsive"
  const ctx = await browser.newContext({ ...devices["Pixel 7"] });
  const mobile = await ctx.newPage();
  await mobile.goto(url, { waitUntil: "networkidle" });
  await mobile.screenshot({ path: `thumbs/${slug}-mobile.png` });
  await ctx.close();
}

await browser.close();
Enter fullscreen mode Exit fullscreen mode

Run it with npx playwright install chromium once, then node scripts/thumbs.mjs. If you want the classic "laptop plus phone" composite, combine the two images in any editor, but keep the layout the same for every item.

Tips:

  • Hide cookie banners and chat widgets before the screenshot (await page.addStyleTag({ content: "#cookie-banner{display:none}" })).
  • Use realistic content. Lorem ipsum in a thumbnail screams "template".
  • Keep the top of the page. Clients judge the hero, the navigation and the typography in a second.

6. Keep it in sync

Because everything lives in a repo, updating is cheap:

  • New project shipped? Add a Markdown file, run the two scripts, paste.
  • Demo site changed? Rerun the scores with a new date.
  • Offer changed? Delete items that no longer match. Old items don't earn you anything if they point clients at work you no longer want.

Where this fits in a freelance profile

A portfolio isn't the whole profile. The title, the first lines of your overview, your skills and your rate do a lot of the filtering before anyone opens an item. Digital Dialogue has a practical walkthrough on how to build an Upwork profile as a web developer that covers the title as an offer, skills as a filter, and why one strong portfolio tile beats six weak ones. The case study template here is meant to be the "strong tile" part of that.

Recap

  • Pick portfolio items that match one clear offer.
  • Can't show client work? Build a small, honest public demo.
  • Write each item as problem, constraints, what I did, result, what I'd change.
  • Use Lighthouse for real numbers, with dates, and Playwright for consistent thumbnails.
  • Keep it all in a repo so updating takes minutes.

The goal isn't to look impressive. It's to make it easy for a client to believe you can do their job.


Abid Ali is a web developer who freelances from Pakistan and writes practical freelancing guides at Digital Dialogue.

Top comments (0)