Takazudo Modular Docs

Type to search...

to open search from anywhere

Image Workflow

Image Workflow

Daily reference for managing product images. For CDN architecture details, see CDN Architecture.

Quick Reference

flowchart LR
    A["/imgs/slug.heic"] -->|pnpm convimgs:upload| B["static/images/p/slug/ + R2"]
    B -->|Netlify proxy| C["takazudomodular.com/images/p/slug/*"]
TaskCommand
Add/update imagepnpm convimgs:upload photo.heic
Process all + upload allpnpm convimgs:upload
Upload only (no processing)pnpm r2:upload
Download from R2 (fresh clone)pnpm r2:download
Check for orphans on R2pnpm r2:check-orphans
Delete orphans from R2pnpm r2:check-orphans:delete
Verify R2 syncpnpm r2:verify-sync
Full metadata update to main/l-metadata-update (Claude Code skill)

Safety Rules

  • NEVER put files directly into static/images/p/ — always go through /imgs/ → convimgs pipeline
  • /imgs/ (Dropbox symlink) is the single source of truth for originals
  • static/images/p/ is derived output, can be regenerated anytime
  • R2 is the canonical remote store — no auto-delete, cleanup is always manual with confirmation

Adding / Updating Images

# 1. Drop source image into /imgs/ (Dropbox symlink)
#    Supports: jpg, png, webp, heic, gif, avif

# 2. Process + upload in one step
pnpm convimgs:upload new-product.heic    # single file
pnpm convimgs:upload                     # all files

# 3. Rebuild metadata database
pnpm build:metadata

For updating, replace the source file in /imgs/ (same filename = same slug), then run the same commands. The upload script uses incremental sync (MD5/ETag comparison), so only changed files are transferred.

Updating metadata-db.json on main

After processing and uploading new images, metadata-db.json must be updated and merged to main so that all branches can use the new image metadata (dimensions, blurhash, etc.). Use the Claude Code skill:

/l-metadata-update

This skill can be invoked from any branch and automates the full flow:

  1. Cleans static/images/p/ and downloads existing images from R2 (ensures only new images are processed)
  2. Runs convimgs:upload (process new images + upload to R2)
  3. Runs build:metadata to regenerate metadata-db.json
  4. Creates a metadata-update-YYMMDD-HHMM branch from main, commits, creates a PR, and merges
  5. Returns to the original branch

The metadata-update-* branch naming convention skips CI checks (pr-checks and security), since these branches only change metadata-db.json.

After merging, merge main into your working branch to pick up the metadata:

git fetch origin main && git merge origin/main --no-edit

Without this step, ResponsiveImage components won’t have blurhash placeholders or responsive srcsets for the new images. The images will still display (R2 serves them), but without the optimized loading experience.

Flags like --cleanup, --skip-ogp, --skip-mercari are passed through to convimgs:

pnpm convimgs:upload --cleanup photo.heic

Deleting Images

There is no auto-delete. R2 cleanup is always manual with confirmation:

  1. Remove the source from /imgs/
  2. Remove local static/images/p/slug/
  3. Run orphan detection to clean R2:
# Report orphans (R2 slugs with no original in /imgs/)
pnpm r2:check-orphans

# Delete orphans with interactive confirmation
pnpm r2:check-orphans:delete

--delete requires /imgs/ to be available (Dropbox symlink). On machines without /imgs/, the orphan check reports local-only orphans but cannot determine R2 orphans.

Fresh Clone / New Worktree

On a fresh clone, static/images/p/ is empty. Two options:

Option A: Download from R2 (recommended)

pnpm r2:download                        # all images
pnpm r2:download -- --slug oxi-one-mk2  # single slug
pnpm r2:download:dry                    # preview only

Option B: Dev without local images

  • pnpm build works fine (production uses R2 CDN URLs)
  • pnpm dev works but product images will be broken locally (metadata/blurhash still available from metadata-db.json)

Pre-Push Verification

run-b4push.sh runs verify-r2-sync.mjs automatically — warns (non-blocking) if local slugs are missing from R2. These images would cause 404s in production.

To check manually:

pnpm r2:verify-sync

Image Formats

Each slug produces these files in static/images/p/slug/:

FilePurpose
600w.webp, 900w.webp, 1200w.webp, 1600w.webp, 2000w.webpResponsive images
ogp.jpgSocial sharing image (1200x630) — only from __og / __ogonly files
mercari.pngMarketplace listing image (1600px width)
metadata.jsonDimensions, blurhash, file paths
original.gifAnimated GIF originals (when applicable)

Animated GIF handling

GIFs bypass the normal WebP conversion to preserve animation. The pipeline copies the original file as-is:

static/images/p/gif-slug/
├── metadata.json    # hasVariants: false, originalFormat: "gif"
└── original.gif     # Original file copied directly

Key differences from regular images:

  • No WebP variants — no 600w.webp etc.
  • No Mercari PNG and no OGP generated
  • hasVariants: false in metadata — signals ResponsiveImage to serve original.gif instead of a <picture> srcset
  • Blurhash is still generated for placeholder display

The Img MDX component extracts the slug from any /images/p/ path and routes to ResponsiveImage regardless of whether __metadata was injected by the remark plugin. ResponsiveImage then loads metadata via getImageMetadata(slug) and constructs the path /images/p/{slug}/original.{originalFormat}.

OGP Image Convention (__og / __ogonly suffixes)

The image pipeline uses __ (double underscore) as a delimiter for conversion rules. Three file types control OGP generation:

Source fileSlugGenerates
foobar.heicfoobarWebP + mercari + metadata (no OGP)
foobar__og.heicfoobar__ogWebP + mercari + metadata + ogp.jpg
foobar__ogonly.heicfoobar__ogonlyogp.jpg ONLY
  • No suffix — regular image, OGP skipped for faster processing
  • __og — full processing plus OGP. Use for product thumbnails that also serve as social sharing images. One source file generates everything needed
  • __ogonly — OGP only, no WebP or mercari variants. Use for dedicated OGP images (e.g., brand OGP for site-wide default)
  • No slug stripping — the slug IS the filename without extension (e.g., foobar__og.heic → slug foobar__og)
  • The --force-ogp CLI flag can force OGP generation on regular files

Generation methods

The processor automatically chooses between two OGP generation methods based on the source image’s aspect ratio:

Composite method (square/portrait sources, aspect ratio < 1.5):

The source image is centered at 600px on a 1200x630 canvas. The background is filled with a heavily blurred and darkened version of the same image, creating a visually appealing card. A subtle drop shadow is added to the foreground.

Composite OGP example — square source with blurred background

Landscape crop method (landscape sources, aspect ratio >= 1.5):

The source image is simply resized and cropped to fit 1200x630, since it already has a wide aspect ratio suitable for OGP.

Landscape OGP example — wide source cropped to fit

The threshold is 1.5 (3

ratio). Most product photos are square (1
), so they use the composite method.

Best practices

  • The source image for __og / __ogonly can be any aspect ratio — the processor handles square, portrait, and landscape sources automatically
  • Every page that needs social sharing must have an ogp.jpg — when no ogp.jpg exists, no og:image meta tag is emitted, and the OGP checker will flag this as an error
  • Use __og when you want a single source file to produce both display images and OGP
  • Use __ogonly for dedicated social sharing images that don’t need display variants

Example

# In /imgs/ directory:
oxi-one-mk2.heic           # → slug: oxi-one-mk2 → WebP + mercari + metadata
oxi-one-mk2__og.heic       # → slug: oxi-one-mk2__og → WebP + mercari + metadata + ogp.jpg
brand-default__ogonly.heic  # → slug: brand-default__ogonly → ogp.jpg ONLY

# Outputs in static/images/p/:
# static/images/p/oxi-one-mk2/         — 600w-2000w.webp, mercari.png, metadata.json
# static/images/p/oxi-one-mk2__og/     — 600w-2000w.webp, mercari.png, metadata.json, ogp.jpg
# static/images/p/brand-default__ogonly/ — ogp.jpg only

OGP image behavior

When a product page is shared on social media:

  1. If ogp.jpg exists → use it (1200x630 JPEG, composite or landscape crop)
  2. If ogp.jpg does not exist → no og:image meta tag is emitted

The OGP checker (check-ogp-images.mjs) runs during CI and treats any page missing og:image as an error. To ensure every page has OGP coverage, use the __og or __ogonly suffix on source images.

MDX Frontmatter Image Fields

MDX articles use frontmatter fields to specify which image slug to use:

FieldPurposeNotes
imgThumbDisplay image slug (hero, thumbnails, cards)Preferred field name
heroImgUrlLegacy alias for imgThumbStill works, imgThumb takes precedence
imgOgpOptional OGP override slugWhen set, OGP uses this instead of imgThumb

Resolution order

  1. imgThumb field (if present)
  2. heroImgUrl field (backward-compatible fallback)
  3. Product image (if product field is set)
  4. No image (OGP tag omitted)

OGP resolution

When generating Open Graph meta tags:

  1. If imgOgp is set → use that slug’s ogp.jpg
  2. If imgOgp is not set → use imgThumb/heroImgUrl slug’s ogp.jpg
  3. If the resolved slug has no ogp.jpg → no og:image meta tag is emitted

Example

---
title: About Takazudo Modular
imgThumb: bird
imgOgp: bird-ogp__ogonly
---

In this example, the page displays bird as the hero image, but when shared on social media, it uses the dedicated bird-ogp__ogonly OGP image.

R2 Environment Variables

All R2 scripts require these env vars (from .env or environment):

VariableRequiredPurpose
R2_ACCOUNT_IDYesCloudflare account identifier
R2_ACCESS_KEY_IDYesS3-compatible access key
R2_SECRET_ACCESS_KEYYesS3-compatible secret key
R2_BUCKET_NAMENoBucket name (default: zmodmedia)