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

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 codeMeaning
0No placeholders found, or --exit-zero was given
1Placeholders found
2Usage 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: .
OptionMeaning
--hosts a.com,b.comReplace the default host list
--add-host HOSTAdd a host (repeatable)
--allow PATTERNSkip URLs that contain PATTERN or match it as a glob, with or without the scheme (repeatable). E.g. --allow 'picsum.photos/seed/*'
--ignore GLOBSkip paths matching GLOB, relative to each PATH (repeatable). E.g. --ignore 'docs/**', --ignore '*.md'
--no-default-ignoresAlso scan node_modules, .git, .hg, .svn and *.map
--ext EXTTreat another extension as text (repeatable)
--no-images / --no-textRun only one of the two checks
--max-size MBSkip larger files (default 20)
--jsonMachine-readable report on stdout
--githubGitHub Actions ::error annotations; on by default when GITHUB_ACTIONS=true
--exit-zeroReport but exit 0; annotations become warnings
-q, --quietPrint 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.