If your project already has a Makefile, adding file conversion is a natural fit. Make's pattern rules, dependency tracking, and parallel execution make it well-suited to managing derived assets. The key insight: WebP files are derived artifacts — like .o files from .c source. Make already knows how to handle this relationship.

TL;DR

PNG_FILES := $(wildcard assets/*.png)
WEBP_FILES := $(PNG_FILES:.png=.webp)

.PHONY: webp clean
webp: $(WEBP_FILES)

%.webp: %.png
	curl -sf --max-time 60 \
	  -H "Authorization: Bearer $(CTF_API_KEY)" \
	  -F "file=@$<" -F "target=webp" \
	  -o "$@" \
	  https://changethisfile.com/v1/convert

clean:
	rm -f $(WEBP_FILES)

Run with: CTF_API_KEY=ctf_sk_... make webp -j8

The use case

Build pipelines are the canonical use case for Makefiles. If your project builds HTML from templates, compresses CSS, or minifies JavaScript, it already needs to manage derived files. Image conversion fits the same model: source images are inputs, optimized formats are outputs, and you only want to regenerate outputs when the source changes.

Make's dependency tracking is the key advantage over bash loops: make webp re-converts only the PNGs that are newer than their corresponding WebP. On a repo with 500 images where 3 changed, only 3 API calls are made — not 500.

This pattern is useful in:

  • Static site generators that don't have built-in asset pipelines
  • Documentation build systems (Sphinx, MkDocs, Hugo) where image optimization is a manual step
  • C/C++ or embedded projects that ship documentation or UI assets alongside binary output
  • Any project where a Makefile is already the build orchestrator

Complete Makefile

# Makefile for asset conversion via ChangeThisFile API
#
# Usage:
#   make webp          - Convert all PNGs to WebP (only stale files)
#   make pdfs          - Convert all DOCX files to PDF
#   make assets        - Build all derived assets
#   make clean         - Remove all generated files
#   make -j8 webp      - Convert with 8 parallel jobs
#
# Environment:
#   CTF_API_KEY        - ChangeThisFile API key (required)

CTF_API_URL  := https://changethisfile.com/v1/convert
CTF_API_KEY  ?= $(error CTF_API_KEY is required)

# ---- Image assets ----------------------------------------------------------
PNG_SOURCES  := $(shell find assets/images -name '*.png' 2>/dev/null)
WEBP_OUTPUTS := $(PNG_SOURCES:.png=.webp)

# ---- Document assets -------------------------------------------------------
DOCX_SOURCES := $(shell find docs/source -name '*.docx' 2>/dev/null)
PDF_OUTPUTS  := $(DOCX_SOURCES:.docx=.pdf)

# ---- Top-level targets -----------------------------------------------------
.PHONY: assets webp pdfs clean

assets: webp pdfs

webp: $(WEBP_OUTPUTS)

pdfs: $(PDF_OUTPUTS)

# ---- Pattern rules ---------------------------------------------------------

# PNG -> WebP
%.webp: %.png
	@echo "Converting $< -> $@"
	@curl -sf \
	  --max-time 90 \
	  -H "Authorization: Bearer $(CTF_API_KEY)" \
	  -F "file=@$<" \
	  -F "target=webp" \
	  -o "$@.tmp" \
	  $(CTF_API_URL) && mv "$@.tmp" "$@" || { rm -f "$@.tmp"; exit 1; }
	@echo "  -> $@ ($(shell du -h $@ | cut -f1))"

# DOCX -> PDF
%.pdf: %.docx
	@echo "Converting $< -> $@"
	@curl -sf \
	  --max-time 120 \
	  -H "Authorization: Bearer $(CTF_API_KEY)" \
	  -F "file=@$<" \
	  -F "target=pdf" \
	  -o "$@.tmp" \
	  $(CTF_API_URL) && mv "$@.tmp" "$@" || { rm -f "$@.tmp"; exit 1; }

# ---- Clean -----------------------------------------------------------------
clean:
	@echo "Removing $(words $(WEBP_OUTPUTS) $(PDF_OUTPUTS)) generated files"
	rm -f $(WEBP_OUTPUTS) $(PDF_OUTPUTS)

Note: Makefile recipes must be indented with a literal tab, not spaces.

Error handling and atomicity

The pattern -o "$@.tmp" && mv "$@.tmp" "$@" || { rm -f "$@.tmp"; exit 1; } is important: it writes to a temp file and renames atomically. If the curl fails or is interrupted, the .tmp file is removed and the target file doesn't exist — so Make knows the target is not built and will try again on the next run.

Without this, a failed partial write would create a corrupt output file with a newer timestamp than the source, causing Make to incorrectly skip it on the next run.

To add verbose error output on failures:

%.webp: %.png
	@STATUS=$$(curl -sf \
	  --max-time 90 \
	  -w "%{http_code}" \
	  -H "Authorization: Bearer $(CTF_API_KEY)" \
	  -F "file=@$<" \
	  -F "target=webp" \
	  -o "$@.tmp" \
	  $(CTF_API_URL)); \
	if [ "$$STATUS" = "200" ] && [ -s "$@.tmp" ]; then \
	  mv "$@.tmp" "$@"; \
	else \
	  rm -f "$@.tmp"; \
	  echo "ERROR: HTTP $$STATUS for $<" >&2; \
	  exit 1; \
	fi

Integration with CI and other Make targets

In a GitHub Actions workflow:

name: Build assets
on:
  push:
    paths:
      - 'assets/images/**/*.png'
      - 'docs/source/**/*.docx'

jobs:
  build-assets:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Convert assets
        env:
          CTF_API_KEY: ${{ secrets.CTF_API_KEY }}
        run: make assets -j8
      - name: Commit converted assets
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add assets/images/*.webp docs/source/*.pdf
          git diff --staged --quiet || git commit -m "chore: regenerate converted assets"
          git push

The workflow only triggers when source files change (the paths filter), and the final step only commits if there are actual changes (git diff --staged --quiet).

To integrate with a larger build target:

build: assets compile package

assets: webp pdfs

compile:
	npm run build

package: build
	docker build -t myapp:latest .

Production tips

  • make -j8 runs 8 conversions in parallel. Make distributes independent targets across jobs. For a flat list of images, all conversions are independent and -j8 gives near-linear speedup up to network saturation.
  • make -k continues past errors. By default, Make stops at the first failure. make -k (keep going) processes all files and reports all failures at the end. Useful for batch jobs where partial results are acceptable.
  • Use $? (newer prerequisites) for incremental builds. In complex dependency graphs, $? gives you the list of prerequisites newer than the target. For simple pattern rules, Make's default behavior (if target is older than source, rebuild) is sufficient.
  • Add generated files to .gitignore. Pattern-generated WebP files are derived artifacts. Either commit them (for static site repos where CI doesn't run the converter) or gitignore them (for repos where CI always regenerates them).
  • Print a summary with make -n first. make -n (dry run) prints what would be executed without running anything. Use it to verify that only stale files would be regenerated before a large batch run.

Make's dependency tracking turns file conversion from a manual chore into a declarative build step. Run make assets -j8 and only the stale files convert — no guessing, no full rebuilds. Get a free API key and add these targets to your existing Makefile today.