From tinify-cli to imgasset: Turning Blog Images into a Pipeline

How a repeated blog image workflow became two reusable npm CLI tools: tinify-cli for compression and imgasset for AI generation, raw asset storage, compression, and publishing.

While adding images to recent blog posts, I ran into a familiar kind of work: the process itself was not difficult, but it had too many small steps, and the steps were almost the same every time.

First, read the article and write prompts. Then generate raw images with an image model. The originals should not go into the repository, so they need to stay in a temporary directory. The selected images need to be compressed, converted into a web-friendly format, and placed under public/assets/. Finally, the Markdown image paths need to be added, followed by a build and link check.

Doing this manually for one or two posts is fine. After enough posts, it becomes repetitive work.

So I split the workflow into two tools:

They were not designed as polished products from day one. They came out of the real process of writing and maintaining this blog, one layer at a time.

Chinese version of this article

AI image generation, compression, and publishing directories organized as one image asset pipeline

To run the full workflow, install imgasset first:

npm install -g @yigemo/imgasset

imgasset already includes tinify-cli as a dependency. If only image compression is needed, the compression tool can also be installed directly:

npm install -g @yigemo/tinify-cli

First Layer: Standardize Image Compression

Before writing imgasset, I first wrote tinify-cli.

Image compression looks like a small problem, but it appears constantly on content sites. Blog images, official website illustrations, product screenshots, and social sharing images eventually face the same issue: raw files are too large to publish directly.

TinyPNG / Tinify has always worked well for compression. But opening a website, uploading files, downloading results, or copying the same API script across projects is not a good long-term workflow.

The goal of tinify-cli is simple: make compression a stable command.

tinify login

Then compress a directory:

tinify temp/article/raw \
  --recursive \
  --out-dir public/assets/article \
  --format jpeg \
  --background white \
  --suffix ""

Several options matter a lot for blog images.

--recursive preserves the directory structure, which is useful when one article has multiple images.

--out-dir writes compressed images into the publishing directory instead of overwriting originals.

--format jpeg and --background white convert PNG outputs into JPEG files that are usually more suitable for web pages, while handling transparent backgrounds.

--suffix "" keeps final file names clean. A raw file such as temp/article/raw/01-context.png can become public/assets/article/01-context.jpg.

Raw images entering compression and format conversion before reaching the publishing directory

This layer solves repeatable compression and format conversion.

But another question appeared quickly: where should the raw images come from?

Second Layer: AI Image Generation Needs a Workflow Too

When adding images to articles, image generation is only one part of the job.

The surrounding workflow is where the friction lives:

  • Prompts should be saved and reused.
  • Generated originals need a fixed location.
  • Existing images should not be regenerated accidentally.
  • API keys should never enter the project repository.
  • Model, size, quality, proxy, and base URL settings should be reusable.
  • After generation, images should be able to move directly into compression.

If every project gets a temporary script, these details scatter quickly. After a while, the scripts become their own maintenance problem: which one still works, which one is outdated, which one hard-codes local paths, and which one might contain sensitive configuration.

imgasset handles this layer.

It splits image generation configuration into three parts:

  • Global profile: base URL, model, size, quality, proxy, and other non-sensitive settings.
  • Global secret: API key, kept outside project repositories.
  • Project configuration: raw directory, publishing directory, and compression options for the current project.

Initialize configuration:

imgasset config init

Create a profile:

imgasset profile set default \
  --base-url https://api.example.com/v1 \
  --model gpt-image-2 \
  --size 1536x1024 \
  --quality medium \
  --output-format png \
  --default

Save the API key:

imgasset secret set default

Then write a JSONL prompt file inside the project:

{"out":"01-context.png","prompt":"Minimal surreal isometric 3D editorial poster..."}
{"out":"02-flow.png","prompt":"Minimal surreal isometric 3D editorial poster..."}

Generate raw images:

imgasset generate prompts.jsonl \
  --raw-dir temp/imgasset/article/raw \
  --skip-existing

--skip-existing is especially useful. Image generation can be slow, and network failures are common enough to design for. If a batch fails on the third image, the next run should not regenerate the first two.

Connect the Two Steps

Using tinify-cli and imgasset generate separately already covers most cases. The smoother version is one command for the whole workflow:

imgasset run prompts.jsonl \
  --raw-dir temp/imgasset/article/raw \
  --publish-dir public/assets/article \
  --format jpeg \
  --background white \
  --skip-existing

This command generates the originals first, then uses the bundled tinify-cli dependency for compression and format conversion.

Prompts, raw image storage, and the publishing directory connected into one continuous production path

In practice, installing imgasset gives a project both image generation and compression. There is no need to maintain a separate compression script in every repository.

I usually keep the directory structure like this:

temp/
  imgasset/
    my-article/
      raw/
public/
  assets/
    posts/
      2026/
        my-article/
prompts.jsonl

temp/ holds originals and temporary files, and stays in .gitignore.

public/assets/ holds compressed publishing assets that can be referenced by articles.

Markdown only references the final output:

![Content system illustration](/assets/posts/2026/my-article/01-context.jpg)

That boundary matters. Originals are production material. Published images are website assets.

Why Not Just Use One Script

At the beginning, a single script is absolutely enough.

For one project, it is often the fastest option. Read an API key from the environment, loop over image generation requests, then call the compression API. A few dozen lines of code can work.

The problem is that this kind of script often becomes a disposable asset.

When a second project needs the same workflow, the script gets copied. Then paths change, model names change, compression formats change, proxy settings change, and error handling changes. After a few rounds, it becomes hard to know which copy represents the current practice.

The value of turning this into a tool is not fewer lines of code. The value is fixing the boundaries:

  • API keys never enter projects.
  • Originals default to temporary directories.
  • Prompts are saved as JSONL.
  • Output paths are controlled by commands or project config.
  • Compression is provided as a dependency instead of a separate setup step.
  • Interrupted runs can continue.

Once these conventions are stable, the same workflow can move across projects.

Security and Open Source

Both tools are published to npm and available on GitHub.

Before open sourcing them, I focused on three things.

First, secrets. API keys should only live in global secret files or environment variables. Project configuration, prompt files, logs, and reports should not contain them.

Configuration, secrets, temporary files, and public assets separated by clear boundaries

Second, examples. Examples should use placeholders such as https://api.example.com/v1, not any real service endpoint from daily use.

Third, publishing. Both packages follow a standard npm package shape. imgasset also uses GitHub Actions and npm Trusted Publishing. Releasing is done with:

pnpm run release

The script increments the version, creates a tag, and GitHub Actions publishes to npm from that tag.

This is a little more structured than running npm publish manually, but it is easier to trace over time. For open source packages, a clear release path is worth the extra setup.

It Still Comes Back to Writing

After finishing these tools, the biggest change was not that I type fewer commands. It was that adding images no longer interrupts the writing rhythm as much.

Previously, the thought of adding images brought a list of small chores: where prompts should live, where originals should go, what the compressed files should be called, whether paths would be wrong, and how to resume after a failed run. Each detail was small, but together they pulled attention away from the article.

Now the workflow feels closer to this:

  1. Read the article and decide how many images it needs.
  2. Write prompts.jsonl.
  3. Run imgasset run.
  4. Insert the output images into Markdown.
  5. Build and check.

The real judgment is still manual: what imagery fits the article, how many images are enough, where an image helps reading, and which attractive image should still be rejected. The tools only fix the directories, commands, format conversion, and retry behavior.

That is what this post is really about: not two npm packages, but a small process becoming stable.

The best small tools are almost invisible in daily use, but immediately useful when moving to another project. tinify-cli handles compression. imgasset connects generation, raw storage, and publishing output. Together they solve not one image generation task, but a reusable image asset workflow for the next article, the next site, and the next project.