Install
openclaw skills install thumbnail-qaThumbnail image QA: browse every page, detect images poorly cropped in their containers, compute fine-grained object-position values, and auto-fix with before/after evidence. Each fix gets its own atomic commit. Use when asked to "check thumbnails", "fix image cropping", "thumbnail qa", or after uploading new images. Proactively suggest when the user adds or replaces images in /public.
openclaw skills install thumbnail-qaThis skill browses every page of the Next.js site, detects images that are poorly cropped
in their containers, computes optimal object-position values based on focal point analysis,
and applies fine-grained CSS fixes — each with a before/after screenshot and its own atomic commit.
# Find browse binary
B=$(command -v browse 2>/dev/null || echo "$HOME/.claude/skills/gstack/browse/bin/browse")
# Verify browse is available
if [ ! -x "$B" ]; then
echo "ERROR: browse binary not found at $B"
echo "Install gstack or ensure browse is on PATH."
exit 1
fi
Run git status --porcelain. If output is non-empty, ask the user:
"Your working tree has uncommitted changes. Before running thumbnail QA, would you like to:
- Commit your changes now
- Stash your changes (git stash)
- Abort and handle it manually
Which option?"
Proceed only after the working tree is clean (or the user explicitly chooses option 3 and understands the risk).
# Check if dev server is running
curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 | grep -q "200\|301\|302" && echo "running" || echo "not running"
If not running, start it:
npm run dev &
DEV_PID=$!
# Wait for server to be ready (up to 30 seconds)
for i in $(seq 1 30); do
curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 | grep -q "200\|301\|302" && break
sleep 1
done
# Set viewport
$B viewport 1280x800
# Create output directory
mkdir -p .gstack/thumbnail-qa/screenshots
# Ensure .gstack/ is in .gitignore
if ! grep -q "^\.gstack/" .gitignore 2>/dev/null; then
echo ".gstack/" >> .gitignore
git add .gitignore
git commit -m "chore: add .gstack/ to .gitignore"
fi
grep -rn "<Image" --include="*.tsx" --include="*.ts" --include="*.jsx" --include="*.js" . \
| grep -v "node_modules" \
| grep "fill"
For each file that contains a match, READ THE ENTIRE FILE using the Read tool (not just a 25-line window). Understanding the full component is required to correctly resolve conditional classNames and dynamic src values.
For each <Image with the fill prop, record:
| Field | Description |
|---|---|
id | Sequential number (1, 2, 3, ...) |
file_path | Absolute path to TSX file |
line_number | Line where <Image starts |
page_route | URL path (e.g., /, /about, /connect) |
src | Value of the src prop (string or expression) |
className | Full className string or expression |
current_position | Extracted object-position class (e.g., object-top, object-[50%_25%], or none) |
container_classes | Classes on the wrapping div (especially overflow-hidden, aspect-, h-, w-*) |
conditional_key | If className is conditional, the field/expression it keys on |
notes | Any dynamic/conditional complexity |
Skip images where:
fill prop presentsrc contains "logo" (case-insensitive)rounded-full class (avatar/icon treatment)object-contain (intentionally letterboxed)Dynamic src (e.g., src={photo.src} or src={item.image}):
Conditional className (e.g., className={category.id === 'worship' ? 'object-top' : 'object-center'}):
conditional_key as the field driving the condition (e.g., category.id)Print a numbered list of all candidate images:
IMAGE REGISTRY (N candidates)
================================
[1] src: /images/team/alice.jpg
route: /about
file: components/TeamSection.tsx:42
current position: object-top
container: relative overflow-hidden h-64 w-full
[2] src: (dynamic) photo.src — 6 entries from data/photos.ts
route: /gallery
file: components/Gallery.tsx:18
current position: object-center (static)
conditional_key: none
notes: expand 6 photo entries individually
...
Expected total: ~15 entries across the full site.
Group all registry entries by page_route to minimize browser navigation (visit each page once).
For each page:
# Navigate
$B goto http://localhost:3000/PAGE_ROUTE
# Wait for images to load
sleep 1
# Screenshot the PARENT DIV (overflow-hidden container), not the img tag
$B screenshot ".gstack/thumbnail-qa/screenshots/IMAGE_ID-current.png" \
--selector "div.relative.overflow-hidden:has(img[src*='FILENAME'])"
If the selector fails (dynamic src, complex DOM), fall back to:
$B screenshot ".gstack/thumbnail-qa/screenshots/IMAGE_ID-current.png" \
--selector "CLOSEST_IDENTIFIABLE_PARENT"
Document selector used in analysis notes.
Use the Read tool to view the original image from public/:
Read: public/images/PATH_TO_IMAGE.jpg
This gives a visual view of the full uncropped image.
Analyze the full image and classify:
| Photo Type | Focal Point Priority Rules |
|---|---|
| Person / Portrait | Face fully visible, forehead to chin. Eyes positioned in upper third of container. |
| Group / Team | Maximize number of visible faces. Favor horizontal center. Avoid cutting anyone. |
| Architecture / Building | Show full structure. Roofline and entryway both visible when possible. |
| Worship / Activity / Event | Keep primary action or speaker in frame. Avoid cropping hands/gestures. |
| Landscape / Scene | Key subject centered; horizon placement depends on sky vs ground interest. |
Record focal point as percentage coordinates: X_PERCENT% Y_PERCENT% (e.g., 50% 25%).
Compare the screenshotted crop against the focal point rules. Determine:
Do NOT mark an image as NEEDS_FIX if the current position already satisfies the focal point rules. If object-top shows the face correctly, leave it. Do not replace a working value with a computed one that might be worse. The goal is curated results, not uniform syntax.
How object-position works: object-position: X Y aligns the X% point of the image to the X% point of the container, and the Y% point of the image to the Y% point of the container.
object-top (= 50% 0%) pins the top of the image to the top of the container. Best for tall portraits where the face is in the upper portion.object-center (= 50% 50%) centers the image. Works when the subject is in the middle.object-bottom (= 50% 100%) pins the bottom.Key insight for tall portrait images in short wide containers: The container crops a horizontal band from the middle of the image by default. To show content near the TOP of a tall image, use object-top or very low Y values (0-10%). A value like object-[50%_20%] does NOT mean "show the top 20%" — it shifts the view DOWN, which is the opposite of what you want for faces near the top of a portrait.
Rules of thumb:
object-top or object-[50%_5%]object-center is fineobject-top, object-center) are preferred when they work — only use arbitrary values when fine-tuning is genuinely neededRound to nearest 5% for cleaner values.
Record per-image analysis:
[1] alice.jpg — NEEDS_FIX
photo type: portrait (tall image, face near top)
focal point: 50% 20% (face, eyes near top of image)
current: object-center (face cut off — container shows middle of image)
recommended: object-top
reason: Face is in upper portion of tall portrait — object-top pins the top of the image to show the face
[2] group-photo.jpg — OK
photo type: group
focal point: 50% 40% (faces clustered in center)
current: object-top
recommended: keep current
reason: object-top already shows all faces — do not replace a working value
Process each NEEDS_FIX image in order.
$B goto http://localhost:3000/PAGE_ROUTE
sleep 1
$B screenshot ".gstack/thumbnail-qa/screenshots/IMAGE_ID-before.png" \
--selector "div.relative.overflow-hidden:has(img[src*='FILENAME'])"
Select the correct edit pattern based on the registry entry:
Pattern A — Static className string
Find the existing object-* class (or position to insert one) and replace/add:
// Before
className="relative overflow-hidden h-64 object-center"
// After
className="relative overflow-hidden h-64 object-[50%_25%]"
Use the Edit tool. Do not change any other part of the className.
Pattern B — Conditional ternary (keyed on a field)
Replace the ternary with a position map that keys on the SAME field as the original condition:
// Before (keyed on category.id)
className={`... ${category.id === 'worship' ? 'object-top' : 'object-center'}`}
// After (position map, same key: category.id)
const positionMap: Record<string, string> = {
worship: 'object-[50%_20%]',
music: 'object-[50%_35%]',
community: 'object-[50%_45%]',
}
// In JSX:
className={`... ${positionMap[category.id] ?? 'object-center'}`}
If the conditional keys on photo.caption or similar string content rather than an ID, prefer adding a position field to the data objects and reading from that instead.
Pattern C — Existing object-[X_Y] arbitrary value
Replace only the arbitrary value portion:
// Before
object-[50%_50%]
// After
object-[50%_25%]
sleep 2
$B screenshot ".gstack/thumbnail-qa/screenshots/IMAGE_ID-after.png" \
--selector "div.relative.overflow-hidden:has(img[src*='FILENAME'])"
Display both before and after screenshots inline. Then re-apply the focal point rules from Step 2.4 against the AFTER screenshot:
If the after screenshot fails any focal point rule that the BEFORE screenshot passed: the fix made things WORSE. This is the most common failure mode — the computed position looked right on paper but the actual crop is worse.
MANUAL_REVIEW with a note: "computed position {X} degraded framing vs original {Y}"If the after screenshot is ambiguous (marginal improvement, unclear if better): mark as MANUAL_REVIEW rather than committing. Err on the side of keeping the original position.
Only proceed to commit if the after screenshot is CLEARLY better than the before.
After a confirmed good fix:
git add PATH/TO/CHANGED_FILE.tsx
git commit -m "style: reposition FILENAME thumbnail — REASON"
# Example:
# git commit -m "style: reposition alice.jpg thumbnail — pull frame up to keep face centered"
One commit per image. Exception: when multiple images share the same conditional block in one file (e.g., a position map covers 6 images in one component), commit them together with a message listing all affected images.
Gather all per-image outcomes:
THUMBNAIL QA REPORT
===================
Date: YYYY-MM-DD
Viewport: 1280x800
Pages checked: N
RESULTS SUMMARY
---------------
Total candidates checked: N
OK (no fix needed): N
Repositioned: N
Skipped (filtered): N
Manual review needed: N
REPOSITIONED IMAGES
-------------------
| # | Image | Page | Before Class | After Class | Reason |
|---|-------|------|--------------|-------------|--------|
| 1 | alice.jpg | /about | object-top | object-[50%_15%] | Face centered with forehead visible |
...
OK IMAGES (no change)
---------------------
| # | Image | Page | Position | Notes |
...
SKIPPED IMAGES
--------------
| # | Image | Reason |
...
MANUAL REVIEW NEEDED
--------------------
| # | Image | Page | Attempted Fix | Issue |
...
COMMITS
-------
abc1234 style: reposition alice.jpg thumbnail — ...
def5678 style: reposition team-photo.jpg thumbnail — ...
SCREENSHOTS
-----------
.gstack/thumbnail-qa/screenshots/
REPORT_DATE=$(date +%Y-%m-%d)
REPORT_PATH=".gstack/thumbnail-qa/report-${REPORT_DATE}.md"
# Write the structured report above to $REPORT_PATH
"Thumbnail QA complete. N images repositioned across N pages. Report saved to
.gstack/thumbnail-qa/report-YYYY-MM-DD.md. Run/thumbnail-qaagain after your next image upload."