tools.placeholder.com/check
placeholder-check
Fails your build when placeholder images would ship to production. One Python 3.8+ file, no dependencies. Run it on your build output in CI.
curl -fsSL https://tools.placeholder.com/check/placeholder-check | python3 - dist
Or download it once and keep it in your repository:
curl -fsSLO https://tools.placeholder.com/check/placeholder-check
python3 placeholder-check dist
What it finds
- Placeholder URLs in text files: HTML, CSS, JS/TS, JSX, Vue, Svelte, Astro, Markdown/MDX, JSON, XML, YAML, templates and more. Default hosts:
via.placeholder.com,placeholder.com,data.placeholder.com,placehold.co,placehold.it,placekitten.com,picsum.photos,dummyimage.com,fakeimg.pl,source.unsplash.comandloremflickr.com, including subdomains. URLs are matched after percent-decoding, so/_next/image?url=https%3A%2F%2Fplacehold.co%2F600x400is caught, and so are JSON-escaped URLs (https:\/\/…). - Downloaded placeholder images: PNG, JPEG, GIF, WebP, AVIF and SVG files carrying the marker that via.placeholder.com embeds in every avatar, photo, QR code and barcode, and brand logo: the text
Placeholder image from via.placeholder.com - replace before shipping. Copying the file intopublic/doesn't hide it, and neither does a bundler inlining it as adata:URI or pasting SVG markup into HTML.
Classic size-and-colour images (/600x400, also @2x) keep their historical bytes and carry no marker, except in AVIF. They are caught by their URL. A downloaded copy of one is just a grey PNG, so link to them rather than downloading.
Output and exit codes
dist/index.html:12:15: placeholder URL https://via.placeholder.com/150
dist/assets/app.js:1:48213: placeholder URL https://picsum.photos/800/600
dist/img/team-anna.png: image carries the via.placeholder.com placeholder marker
placeholder-check: 3 placeholder reference(s) in 3 file(s) [picsum.photos (1), via.placeholder.com (2)]; scanned 214 file(s)
| Exit code | Meaning |
|---|---|
0 | No placeholders found, or --exit-zero was given |
1 | Placeholders found |
2 | Usage error, or a path that doesn't exist or can't be read |
--json prints {version, ok, exit_code, scanned_files, findings: [{type, file, line, column, url, host, snippet, message}], errors, summary}. Image findings have type: "marker" and line: null.
Options
placeholder-check [options] [PATH ...] # default PATH: .
| Option | Meaning |
|---|---|
--hosts a.com,b.com | Replace the default host list |
--add-host HOST | Add a host (repeatable) |
--allow PATTERN | Skip URLs that contain PATTERN or match it as a glob, with or without the scheme (repeatable). E.g. --allow 'picsum.photos/seed/*' |
--ignore GLOB | Skip paths matching GLOB, relative to each PATH (repeatable). E.g. --ignore 'docs/**', --ignore '*.md' |
--no-default-ignores | Also scan node_modules, .git, .hg, .svn and *.map |
--ext EXT | Treat another extension as text (repeatable) |
--no-images / --no-text | Run only one of the two checks |
--max-size MB | Skip larger files (default 20) |
--json | Machine-readable report on stdout |
--github | GitHub Actions ::error annotations; on by default when GITHUB_ACTIONS=true |
--exit-zero | Report but exit 0; annotations become warnings |
-q, --quiet | Print only the summary line |
To skip one line, put placeholder-check: ignore anywhere on it, for example in a comment. Mentions of tools.placeholder.com (this tool and its docs) are always allowed.
GitHub Actions
- run: npm run build
- name: No placeholder images in the build
run: curl -fsSL https://tools.placeholder.com/check/placeholder-check | python3 - dist
Findings show up as annotations on the pull request. There is also a composite action (action.yml, inputs path and args, output exit-code) for use from a repository you vendor it into:
- uses: ./tools/check # wherever you keep placeholder-check and action.yml
with:
path: dist # several paths: space- or newline-separated
args: --allow 'picsum.photos/seed/*' --ignore 'docs/**'
The action is served next to the script: https://tools.placeholder.com/check/action.yml.
GitLab CI
placeholder-check:
stage: test
image: python:3-alpine
needs: [build]
script:
- wget -qO placeholder-check https://tools.placeholder.com/check/placeholder-check
- python3 placeholder-check --json dist > placeholder-report.json || (python3 placeholder-check -q dist; exit 1)
artifacts:
when: always
paths: [placeholder-report.json]
pre-commit
Vendor the script (for example as tools/placeholder-check) and add a local hook:
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: placeholder-check
name: no placeholder images
entry: python3 tools/placeholder-check
language: system
types_or: [html, css, javascript, jsx, ts, tsx, vue, svelte, markdown, json, svg, png, jpeg, gif, webp]
exclude: ^(docs|fixtures)/
Or a plain git hook:
#!/bin/sh
# .git/hooks/pre-commit (chmod +x)
git diff --cached --name-only --diff-filter=ACM -z | xargs -0 -r python3 tools/placeholder-check -q
In source trees, placeholders in tests, fixtures or Storybook stories are often intentional. Use --ignore or exclude: for those, or point the check at the build output instead.
Checking at runtime
Every image from via.placeholder.com and every response from data.placeholder.com carries an X-Placeholder header. An end-to-end test can fail on it:
// Playwright Test
test("no placeholder assets", async ({ page }) => {
const found = [];
page.on("response", r => { if (r.headers()["x-placeholder"]) found.push(r.url()); });
await page.goto("/");
await page.waitForLoadState("networkidle");
expect(found).toEqual([]);
});
Cross-origin responses only expose this header to page JavaScript if the server lists it in Access-Control-Expose-Headers; test runners like Playwright see all headers.