Skip to content

Sabbir Mohammad Sami web developer

ResumeDownload Resume

All notes

(Performance)14 min read

Fast, SEO-Friendly Website Images: By Hand, or With One Claude Code Skill

Camera photos weigh 15–40 MB each and slow a site down. Look at every photo, give it a descriptive kebab-case name, make 2400 px masters, pre-render AVIF and WebP at fixed widths with hashed file names, serve them with a year-long cache and real alt text. This guide does it by hand, then with a free Claude Code skill.

Cover: “AVIF/WEBP” in heavy capitals over three cards — rename, pre-render and serveCover: “AVIF/WEBP” in heavy capitals over three cards — rename, pre-render and serve

( Before you start )

Why do raw photos make a website slow?

A client sends a folder of photos from a professional shoot. On the restaurant site I built for Café des Artistes that was 70 files, 36 megapixels each, 12–20 MB apiece, 1 GB in total, named Cafe de Artistes 07.02.26-17.jpg — with two spaces in the middle. Dropping them into public/ as they are means every visitor downloads far more pixels than any screen can show, and search engines learn nothing from the file names.

The fix is a short pipeline. Done once, the same photos ended up at 42 KB for a phone-sized AVIF and 225 KB for a full-width 2400 px hero, answered in 14 ms, with names like pizza-margherita-top and alt text that says what is in the frame.

The pipeline at a glance

StepWhat you doWhy
1. LookContact sheets of every photoYou cannot name or place what you have not seen
2. Namecategory-subject-variant, lowercase, EnglishFile names are a search signal; DSC_0042 carries none
3. Masters2400 px long edge, sRGB, JPEG q92 4:4:4One high-quality source for every web size
4. CheckPSNR against the original, plus a 100 % cropProof that nothing visible was lost
5. Pre-renderAVIF + WebP at fixed widths, hashed namesNo encoding when a visitor arrives
6. ServeA loader or <picture>, year-long cacheThe right size and format per browser
7. SEOAlt text, OG image, sizes, hero preloadSearch, social previews, LCP
8. VerifyEvery image URL answers 200, in a browser tooNo broken images ship

You need ImageMagick 7 (brew install imagemagick on macOS, apt install imagemagick on Ubuntu) and, for step 5, Node with sharp in the project (npm i -D sharp).

There are two ways through it. Way 1 walks the eight steps by hand, so you know what each one does and why. Way 2 does all eight with one prompt, using a free Claude Code skill you install from GitHub.

( Way 1: by hand )

Look at every photo

Make contact sheets — many small thumbnails on one image, each labelled with its file name — and go through all of them. Note the subject, whether it is portrait or landscape, whether someone’s face is visible, and which shots are near-duplicates.

Bash
mkdir -p sheets
magick montage -label '%f' raw/*.jpg \
  -tile 6x -geometry 300x300+6+6 -pointsize 14 sheets/sheet-%02d.jpg

-label has to come before the input files, or the tiles get no captions. With 40 MP originals this is slow; the skill in Way 2 decodes each file at reduced size first, which is many times faster.

Name every photo for people and search engines

Google reads image file names as a light hint about the content, and a developer reads them every day. Use a category prefix, then the subject, then the variant:

Instead ofUse
Cafe de Artistes 07.02.26-17.jpgdrink-espresso-martini.jpg
DSC_0042.jpgpizza-margherita-top.jpg
IMG_20251125_1914.jpginterior-table-setting.jpg
final-final-2.jpgdessert-cheesecake-sugar-dust.jpg
  • Lowercase kebab-case, English, ASCII — no spaces, dates or camera numbers.
  • Variants of one subject get a suffix: -top, -close-up, -hand, -steam.
  • People get their own prefix (team-, guest-), so they are easy to leave out when a client prefers no faces on the site.
  • Honest names: if you are not sure what a dish is, use the safest accurate description and ask.

Write the decisions into a two-column CSV, original,name. It drives the next step and doubles as a record of which original became which file.

Make 2400 px masters, and keep the originals safe

A master is the single high-quality copy every web size is made from. 2400 px on the long edge covers a full-width image on a 1440 px retina screen. JPEG at quality 92 with 4:4:4 chroma keeps saturated reds — tomato, wine — from smearing.

Bash
mkdir -p assets/photos
magick "raw/DSC_0042.jpg" -auto-orient -colorspace sRGB -filter Lanczos \
  -resize '2400x2400>' -strip -interlace JPEG \
  -sampling-factor 4:4:4 -quality 92 assets/photos/pizza-margherita-top.jpg
  • -auto-orient applies the camera’s rotation flag before it is stripped, so portrait shots stay portrait.
  • -strip removes camera metadata (often 50–100 KB of it, and sometimes GPS coordinates).
  • assets/photos/ is not served — masters are inputs, not something visitors download.

Never delete the originals. Move them out of public/ (everything there is deployed and bloats the repository) into a folder outside the project, and back it up. They are the only copy at full resolution.

Check that nothing visible was lost

Compare a master with a lossless resize of its original. PSNR (peak signal-to-noise ratio) gives one number in decibels:

Bash
magick "raw/DSC_0042.jpg" -auto-orient -filter Lanczos -resize '2400x2400>' -depth 8 ref.ppm
magick compare -metric PSNR ref.ppm assets/photos/pizza-margherita-top.jpg null:
  • 40 dB and above — visually lossless. Most photos land at 42–46 dB.
  • 35–40 dB — normal for very fine texture (wood grain, pizza crust). Put a 100 % crop of both side by side; on the café set the 36 dB pizza was indistinguishable.
  • Under 15 dB — the comparison is wrong, not the image: a rotated reference or a mismatched pair.

Pre-render AVIF and WebP at fixed widths

AVIF came out 25–50 % smaller than WebP at the same measured quality on these photos; WebP covers the few browsers without AVIF. Render both at every width the site asks for, and put a hash of the master in the file name so a changed photo gets a new URL:

JavaScript
// scripts/build-images.mjs — run with: node scripts/build-images.mjs
import { createHash } from 'node:crypto';
import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import sharp from 'sharp';

const WIDTHS = [256, 384, 640, 828, 1200, 1600, 2048, 2400];
const SRC = 'assets/photos';
const OUT = 'public/images/opt';
const manifest = {};

rmSync(OUT, { recursive: true, force: true });
mkdirSync(OUT, { recursive: true });

for (const file of readdirSync(SRC).filter((f) => f.endsWith('.jpg'))) {
  const name = file.replace(/\.jpg$/, '');
  const input = readFileSync(`${SRC}/${file}`);
  const hash = createHash('sha256').update(input).digest('hex').slice(0, 8);
  const { width } = await sharp(input).metadata();
  const widths = [...new Set([...WIDTHS.filter((w) => w < width), width])];
  for (const w of widths) {
    const img = sharp(input).resize({ width: w });
    const stem = `${OUT}/${name}.${hash}.${w}`;
    await img.clone().avif({ quality: 64, effort: 5, chromaSubsampling: '4:4:4' }).toFile(`${stem}.avif`);
    await img.clone().webp({ quality: 82, effort: 6, smartSubsample: true }).toFile(`${stem}.webp`);
  }
  manifest[name] = { v: hash, widths };
}
writeFileSync('data/image-manifest.json', JSON.stringify(manifest, null, 2));

Add it to package.json as "images": "node scripts/build-images.mjs". 56 photos took about six minutes. Commit the output instead of building it on every deploy — it only changes when a photo does.

Why not let next/image convert to AVIF on the fly?

Next.js can serve AVIF with one line, images.formats: ["image/avif", "image/webp"]. It encodes each image the first time a size is requested — and AVIF is slow to encode. Measured on the café site:

First request, cold cacheTime
One 1920 px AVIF~1 000 ms
Same image as WebP~320 ms
12 AVIFs in parallel (one screen of the home page)2.7 s
Pre-rendered static file14 ms

The site had a preloader that waited for the hero image, so every cold view started with a second of nothing, and it happened again for every new screen width and after every cache expiry. Encoding once, before deploy, removes the wait entirely. Keep on-the-fly optimisation for images you cannot know at build time, such as user uploads.

Serve them from Next.js

Keep <Image> everywhere — fill, sizes, lazy loading and preload all still work — and give it a custom loader that points at the pre-rendered files. In the code, a photo is referred to by name: <Image src="/img/pizza-margherita-top" … />.

TypeScript
// lib/image-loader.ts
'use client';

import manifest from '@/data/image-manifest.json';

const photos: Record<string, { v: string; widths: number[] }> = manifest;

export default function imageLoader({ src, width }: { src: string; width: number }) {
  if (!src.startsWith('/img/')) return `${src}?w=${width}`; // logos, SVGs
  const name = src.slice('/img/'.length);
  const photo = photos[name];
  if (!photo) return src;
  const w = photo.widths.find((x) => x >= width) ?? photo.widths.at(-1);
  return `/img/${name}.${photo.v}.${w}`;
}

In next.config.ts, rewrite /img/… to the AVIF file when the browser’s Accept header lists AVIF and to the WebP file otherwise, and cache both for a year — the hash in the name makes that safe:

TypeScript
images: {
  loader: 'custom',
  loaderFile: './lib/image-loader.ts',
  deviceSizes: [640, 828, 1200, 1600, 2048, 2400], // same widths as the script
  imageSizes: [256, 384],
},
async rewrites() {
  return [
    { source: '/img/:file', has: [{ type: 'header', key: 'accept', value: '.*image/avif.*' }], destination: '/images/opt/:file.avif' },
    { source: '/img/:file', destination: '/images/opt/:file.webp' },
  ];
},
async headers() {
  return [{
    source: '/img/:file',
    headers: [
      { key: 'Cache-Control', value: 'public, max-age=31536000, immutable' },
      { key: 'Vary', value: 'Accept' },
    ],
  }];
},

One trap: anything that loads a photo without <Image> — a canvas animation calling new Image(), a CSS background-image, a hand-written <img> — skips the loader and asks for /img/name, which does not exist. On the café site that was a scroll scene drawn on a canvas; it showed up as three 404s. Search for those and pass them through imageLoader({ src, width }).

Without Next.js: the picture element

On a plain HTML site, Vite, WordPress or anything else, let the browser choose the format itself. No server rules are needed, which also makes this the right choice behind a CDN that ignores Vary: Accept:

Text
<picture>
  <source type="image/avif" sizes="(min-width: 768px) 50vw, 100vw"
          srcset="/images/opt/pizza-margherita-top.3af62bcf.640.avif 640w,
                  /images/opt/pizza-margherita-top.3af62bcf.1200.avif 1200w,
                  /images/opt/pizza-margherita-top.3af62bcf.2400.avif 2400w">
  <img src="/images/opt/pizza-margherita-top.3af62bcf.1200.webp"
       srcset="/images/opt/pizza-margherita-top.3af62bcf.640.webp 640w,
               /images/opt/pizza-margherita-top.3af62bcf.1200.webp 1200w,
               /images/opt/pizza-margherita-top.3af62bcf.2400.webp 2400w"
       sizes="(min-width: 768px) 50vw, 100vw"
       width="2400" height="1600" alt="Pizza Margherita from above"
       loading="lazy" decoding="async">
</picture>

Generate this markup from the manifest with a small helper in your templates rather than typing hashes by hand. Serve /images/opt/ with Cache-Control: public, max-age=31536000, immutable.

The SEO details that are easy to miss

  • Alt text in the site’s language, describing the frame — “Pizza Margherita von oben”, not “food image”. Leave alt="" only on purely decorative backgrounds.
  • A separate Open Graph image: 1200×630 JPEG, around 120 KB. Facebook, LinkedIn and WhatsApp do not reliably show AVIF or WebP link previews.
  • The hero is preloaded, everything below the fold is lazy. The hero is usually the Largest Contentful Paint.
  • `sizes` matches the layout. A 300 px card with sizes="100vw" downloads a 2400 px file on a retina screen.
  • Every image has dimensions or an aspect-ratio box, so the page does not jump while it loads.
  • Photos fit their slots: portrait shots in tall frames, landscape in full-width ones, and an image that matches the text beside it.

Verify before you ship

Bash
npm run build && npx next start -p 3100
curl -sI -H 'Accept: image/avif,image/webp,*/*' \
  http://localhost:3100/img/pizza-margherita-top.3af62bcf.1200
# content-type: image/avif
# cache-control: public, max-age=31536000, immutable
# vary: Accept

Repeat with Accept: image/webp,*/* and expect WebP. Then open the site with the dev tools Network tab filtered to images, scroll to the bottom, and check there is not a single 404.

( Way 2: with the Claude Code skill )

What the skill does

Everything in Way 1 — including the mistakes I hit along the way — is packaged as a Claude Code skill called web-image-pipeline. It holds the instructions and five scripts: contact sheets, masters, the quality check, the AVIF/WebP build, and a page checker that requests every image URL on your pages.

Text
web-image-pipeline/
├── SKILL.md                  # the workflow and the reasons behind each step
├── references/
│   ├── nextjs.md             # loader, rewrites, cache headers, canvas pitfall
│   └── static-html.md        # <picture>, CSS image-set, CDN cache headers
├── scripts/
│   ├── contact_sheet.py      # numbered sheets of every photo
│   ├── prepare_masters.py    # rename + 2400 px masters from a CSV
│   ├── quality_check.py      # PSNR against the originals
│   ├── build_images.mjs      # AVIF + WebP at fixed widths, hashed names
│   └── check_pages.py        # every image URL on your pages answers 200
├── README.md
├── LICENSE                   # MIT
└── CITATION.cff

Install it from GitHub

The skill is open source under the MIT licence: github.com/sabbirsami/web-image-pipeline. First check the three things it needs — ImageMagick 7, Python 3 and Node:

Bash
magick -version | head -1   # ImageMagick 7.x
python3 --version           # Python 3.x
node --version              # any current LTS

Then clone it into your personal skills folder, which makes it available in every project:

Bash
git clone https://github.com/sabbirsami/web-image-pipeline.git ~/.claude/skills/web-image-pipeline

Or install it for one project only, so everyone who clones the project gets it too:

Bash
git clone https://github.com/sabbirsami/web-image-pipeline.git .claude/skills/web-image-pipeline
rm -rf .claude/skills/web-image-pipeline/.git
git add .claude/skills/web-image-pipeline && git commit -m "Add web-image-pipeline skill"

Restart Claude Code, type / and web-image-pipeline is in the list. In each project that uses it, add sharp once with npm i -D sharp. To update a personal install later: git -C ~/.claude/skills/web-image-pipeline pull.

Use it

Start Claude Code in the project and ask in plain words; the skill loads when the request matches. You can also call it directly:

Text
optimise the photos in public/new, rename them properly and use them on the site

the images are heavy and the site loads slowly, fix it

/web-image-pipeline public/new — avoid photos that show people's faces
  1. It reads the project first: how images are rendered, existing names, the site’s language, where it deploys.
  2. It looks at every photo through contact sheets, then proposes names and tells you which ones are guesses.
  3. It makes the masters, moves the originals out of public/ and tells you exactly where they went.
  4. It measures the quality and pre-renders AVIF and WebP.
  5. It wires them in — loader and config for Next.js, <picture> elsewhere — and places photos by shape and by the text beside each slot.
  6. It builds, checks every image URL, loads the site in a browser and reports sizes, quality and anything it changed.

Adding a photo later takes three steps: put the JPEG in assets/photos/, run npm run images, then use src="/img/<file-name>".

( Questions )

Image optimisation FAQ

Should I use AVIF or WebP?
Both. AVIF is 25–50 % smaller at the same quality and keeps colour detail better; WebP is the fallback for the few browsers without AVIF. Serve AVIF first.
Why 2400 px and not the full camera resolution?
A full-width image on a 1440 px retina screen needs about 2880 px at most, and 2400 px is visually the same there. A 7400 px original is nine times the pixels with nothing to show for it.
Why are there hundreds of new image files in git?
Each photo exists in up to eight widths and two formats — 56 photos became 748 files. A visitor downloads one of them per image: the right size, in a format their browser supports. If the count bothers you, use four widths instead of eight.
Do image file names really help SEO?
A little. Google lists descriptive file names among its image best practices, but alt text and the text around the image matter more. Good names mostly help the next developer.
Will this break behind Cloudflare?
Format switching by Accept header needs a cache that respects Vary: Accept. If yours does not, use the <picture> element, where the browser picks the format itself.

References: Next.js — Image component, sharp — output options, Google — Image SEO best practices.

Topic
Performance
Published
Reading
14 min · 2,477 words
Sections
14
Tags
  • Images
  • AVIF
  • WebP
  • Next.js
  • SEO
  • Claude Code
  • Agent Skills

Experience

( 03 roles — 2+ years )

Full-stack developer in Dhaka, Bangladesh. Building for clients in Germany and Bangladesh since 2022.

  1. (01)

    August 2025 — Present

    Full-Stack Developer

    Susko GmbH · Full Time · Remote

    • Developed and enhanced an AI-powered SaaS and social media automation platform — AI content, image and video generation, automated scheduling and publishing, multi-account and content-team management, Meta, Instagram and Google integrations, WhatsApp-based content generation and scheduling, admin controls and subscription management.
    • Built a full-stack Padel court management and automation system that connects physical court hardware to a real-time web dashboard — court and player management, game control and live scores over WebSocket — and worked on the sensor electronics, PCB design, footprint verification, Gerber validation and JLCPCB manufacturing.
    • Built and maintained automation workflows with n8n, webhooks, APIs and external service integrations that take manual steps out of business processes.
    • Delivered 40+ production websites and full-stack applications for German clients across industries in React, Next.js and TypeScript — shop management, review management and automation, fundraising platforms, menu management, QR code generation and other custom business tools.
    • Optimized backend performance across production applications — database queries, API paths, data processing and Redis-based workflows — for faster, more responsive apps.

    Next.js · React · TypeScript · Node.js · Python · Prisma · Redis · WebSocket · n8n · OpenAI

  2. (02)

    March 2024 — May 2025

    Software Developer

    Bangladesh Associate of IT Solutions · Full Time · On-site

    • Developed a full-stack inventory management system tracking products, sales, payments, returns, waste products, customer ledgers and SMS notifications, in React, TypeScript, Express and MySQL.
    • Scaled an accounting management platform from a single-client application into a multi-user system, with financial dashboards, journal entries, transaction management, reporting and data visualization.
    • Deployed and maintained web applications on cPanel and VPS servers — production deployments, domain configuration, server setup and troubleshooting.
    • Delivered websites for schools, madrasas and other organizations, turning client requirements into responsive, production-ready web applications.
    • Developed and redesigned production and company portfolio websites in React, Next.js and TypeScript, with a focus on responsive design and user experience.

    React · Next.js · TypeScript · Express · MySQL · Tailwind CSS · cPanel · VPS

  3. (03)

    November 2023 — April 2024

    Full-Stack Developer

    Diginie IT · Internship · Remote

    • Completed a remote internship as a full-stack developer, building and maintaining client web applications in Next.js, TypeScript and Tailwind CSS.
    • Turned designs into responsive pages that held together on every screen size, and kept existing client sites running.

    Next.js · TypeScript · Tailwind CSS · GitHub

About

( Dhaka, Bangladesh )

Sabbir Mohammad Sami — full-stack web & app developer at Susko GmbH, building for the web since 2022.

Sabbir Mohammad Sami, smiling and looking off to the side, seated on a white block in a black sweater — black-and-white studio portrait

I’m a full-stack developer from Dhaka who cares about the part people actually touch — unusual layouts, careful CSS, motion that means something — and about the code underneath being just as clean.

( Story )

I grew up in Dhaka and studied arts and humanities at Kabi Nazrul Government College. Alongside my studies I joined a web development course, and that is where the real growth started — surprise. Alongside work, I am now in my third year of Honours in Economics at National University.

My first website was a tourism site about the place I most wanted to visit. I built it in class 10.

Today I build AI products, automation and client platforms at Susko GmbH, after two years of shipping inventory, accounting and client sites at BAITS. What I am after has not changed: interfaces that look good and back-ends that run smoothly, from the same pair of hands.

Honest, down to earth, and fond of hard work. My inbox is always open — for a project, or just to say hi.

  1. ( Education )

    1. Kabi Nazrul Government College

      • Arts and humanities
      • Dhaka, Bangladesh
    2. National University, Bangladesh

      Session 2023–2024 · Now

      • Honours in Economics — 3rd year
      • Department of Economics
  2. ( Co-curricular )

    —

    Bangladesh National Cadet Corps (BNCC)

    • 1 No Battalion, Ramna Regiment
    • Rank: Cadet Corporal
  3. ( Languages )

    Bangla, English, Hindi

    • Bangla — native
    • English — comfortable
    • Hindi — fluent

( Skills )

Expertise
React · Next.js · JavaScript · TypeScript · MongoDB · Bootstrap · Tailwind CSS · shadcn/ui · GitHub · Figma
Comfortable
Flutter · Dart · MySQL · Mongoose · Express · TanStack Query · Firebase · React Router · Photoshop
Familiar
Zod · Framer Motion · GSAP · JWT · Axios · CORS · Stripe · Vercel

( Questions )

Who is Sabbir Mohammad Sami?

Sabbir Mohammad Sami (S M Sami) is a full-stack web and app developer from Dhaka, Bangladesh. Since August 2025 Sami has worked as a Full-Stack Developer at Susko GmbH in Germany, after building inventory and accounting platforms at Bangladesh Associate of IT Solutions (BAITS). Sami has been building for the web since 2022.

What does Sabbir Mohammad Sami build?

Sami builds websites, web apps, mobile apps and AI products end to end: company and restaurant websites, booking and management platforms, an AI social media platform (Kocial), an AI review manager (ReviewSync) and a Flutter money app (Sompi). That includes 40+ production websites for clients in Germany and Bangladesh.

Which technologies does Sami work with?

Sami works across the MERN stack — MongoDB, Express, React and Node.js — and with Next.js, TypeScript, Tailwind CSS, MySQL, PostgreSQL, Prisma and Redis. Mobile apps are built in Flutter and Dart. AI features use the OpenAI API, n8n automations and webhooks, and motion uses GSAP.

Does Sami build mobile apps with Flutter?

Yes. Sami builds cross-platform apps in Flutter and Dart. Sompi, Sami’s own money tracker, runs on Android, iOS, the web and the desktop from one Flutter codebase, with Riverpod for state and an encrypted SQLite store, so every account and transaction stays on the device.

Does Sami build AI products?

Yes. At Susko GmbH Sami builds AI SaaS: Kocial writes captions, generates images and publishes to Facebook and Instagram for agencies, and ReviewSync answers Google reviews in a business’s own voice. Sami also builds n8n workflows that connect AI models to business tools.

How can I hire Sami as a web or app developer in Dhaka?

Email smsami.dev@gmail.com or use the contact form on this site with a short note about the project. Sami works with clients in Dhaka, across Bangladesh and abroad — remotely for companies in Germany — and replies in English or Bangla.

( Contact )

Contact

( Available for projects )

Based in Dhaka, Bangladesh (GMT+6). Happy to talk in English, Bangla or Hindi.

Let’s work together.

Have an exciting project you need help with? Send me an email, give me a call, or write below — a project, a question, or just to say hi.

( Write to me )

* Required