DEV Community

PlacePack
PlacePack

Posted on • Originally published at placepack.top

Stop hardcoding image URLs in your Storybook stories

If you've ever run Chromatic or Percy on a Storybook that pulls images from Lorem Picsum, you know the pain. Every snapshot run fetches a different image. Your baseline diffs never settle. You end up suppressing noise instead of catching real regressions. It's a slow leak that eventually makes your visual testing pipeline worthless.

The fix is boring: use deterministic image URLs. Every run, same image. No surprises.

The problem with random placeholder URLs

Here's a typical story using Lorem Picsum:

// ProductCard.stories.jsx
export const Default = {
  args: {
    title: "Wireless Headphones",
    price: "$79",
    image: "https://picsum.photos/400/300", // different image every time
  },
};
Enter fullscreen mode Exit fullscreen mode

picsum.photos/400/300 serves a random photo on every request. That's fine for visual design work, but it means every snapshot run is comparing against a different image. Your visual diff tool flags the image as changed — because it is changed — and your team either ignores the alert or spends 10 minutes confirming it's noise.

Some teams work around this by seeding Picsum with a fixed ID (picsum.photos/id/42/400/300), but now you're hardcoding a photo of a random European city in your card component stories. That's weird, and it'll break if that photo ever gets removed.

The after: PlacePack API URLs

PlacePack generates placeholder images with labeled dimensions and customizable colors. More importantly: the same URL always returns the same image. It's a pure function over its inputs.

// ProductCard.stories.jsx
export const Default = {
  args: {
    title: "Wireless Headphones",
    price: "$79",
    image: "https://placepack.top/api/v1/product-card:400x300.svg",
  },
};
Enter fullscreen mode Exit fullscreen mode

The label (product-card) renders inside the placeholder. The dimensions are baked in. Run this story 1000 times — same SVG, same pixels, no diff noise.

For PNG snapshots (Chromatic, Percy, Playwright Component Testing), use .png instead:

image: "https://placepack.top/api/v1/product-card:400x300.png"
Enter fullscreen mode Exit fullscreen mode

Real-world example: a component with multiple image slots

Here's a HeroSection story that uses PlacePack for both the hero background and the author avatar:

// HeroSection.stories.jsx
import { HeroSection } from "../components/HeroSection";

const BASE = "https://placepack.top/api/v1";

export default {
  title: "Marketing/HeroSection",
  component: HeroSection,
};

export const Default = {
  args: {
    backgroundImage: `${BASE}/hero-bg:1200x630.png`,
    authorAvatar: `${BASE}/avatar:80x80.png`,
    authorName: "Jane Smith",
    headline: "Ship faster with better placeholders",
  },
};

export const DarkTheme = {
  args: {
    ...Default.args,
    backgroundImage: `${BASE}/hero-bg:1200x630.png?bg=1a1a2e&color=ffffff`,
    authorAvatar: `${BASE}/avatar:80x80.png?bg=1a1a2e&color=ffffff`,
  },
};
Enter fullscreen mode Exit fullscreen mode

The URL parameters bg and color let you customize the fill and text color to match your brand or theme — still deterministic, still stable.

Using the presets endpoint

If you're setting up Storybook for a new project and aren't sure what sizes to use, hit the presets endpoint:

curl https://placepack.top/api/v1/presets
Enter fullscreen mode Exit fullscreen mode

It returns named presets for common use cases — hero banners, thumbnails, avatars, open graph images — with the exact dimensions already filled in. You can use those names directly in your API URLs:

// generate-story-fixtures.mjs
const res = await fetch("https://placepack.top/api/v1/presets");
const { presets } = await res.json();

// presets.storybook contains the sizes relevant to Storybook stories
console.log(presets.storybook);
// [{ label: "hero", width: 1200, height: 630 }, { label: "thumb", width: 400, height: 300 }, ...]
Enter fullscreen mode Exit fullscreen mode

You can run this script once to generate a fixtures.js file that exports all your placeholder URLs keyed by name:

// scripts/generate-placeholders.mjs
import { writeFileSync } from "fs";

const res = await fetch("https://placepack.top/api/v1/presets");
const { presets } = await res.json();

const storybook = presets.storybook ?? [];
const urls = Object.fromEntries(
  storybook.map(({ label, width, height }) => [
    label,
    `https://placepack.top/api/v1/${label}:${width}x${height}.png`,
  ])
);

writeFileSync(
  "src/test-utils/placeholders.js",
  `export const placeholders = ${JSON.stringify(urls, null, 2)};\n`
);
Enter fullscreen mode Exit fullscreen mode

Then import it everywhere:

import { placeholders } from "@/test-utils/placeholders";

export const Default = {
  args: { image: placeholders.thumb },
};
Enter fullscreen mode Exit fullscreen mode

A note on network access in CI

If your Storybook CI environment blocks outbound requests, you have two options:

  1. Use the pack endpoint to download PNG/SVG files locally and commit them as fixtures (covered in the visual regression article).
  2. Allow outbound HTTPS to placepack.top — the API is free, no key needed, and rate-limited to 30 requests per 60 seconds, which is plenty for a story build.

The API requires no authentication. No signup, no token rotation, no surprise 402s because you hit a free tier ceiling.

Try it

The quickest way to see what your stories will look like: open the generator at placepack.top, type a label and size, pick your colors, and copy the API URL directly.

More Storybook-specific guidance — including a Storybook decorator that auto-replaces broken image src values — lives at placepack.top/storybook.

Top comments (0)