The Problem
The SaaS Boat library contains 3,661 screenshots across 258+ products. Without a dedicated tool, adding one screenshot to the library required: open the SaaS app, use a screenshot tool, manually download the file, switch to WordPress, fill in the app name, page type, and title, upload the image, publish the post. Five to ten minutes per screenshot, with metadata that drifts inconsistent over time.
At that rate, 3,661 screenshots would take roughly 300 hours of mechanical work. The extension collapsed that entire process into a side panel that stays open while you browse. Open a SaaS app, see what's already captured, click what's missing, capture, upload. Under 30 seconds. Metadata filled in automatically.
"The 3,661 screenshots in the SaaS Boat library exist because this extension made capturing them fast enough to do at scale. It's not a side project — it's the content production engine for the whole product."
— The real role of this tool in the SaaS Boat systemThe Workflow
The panel is designed so decisions happen before capture, not after. Every step either eliminates a decision or pre-fills an answer.
Open any SaaS product in Chrome
Extension detects the app automatically from the URL using hostname matching and brand token extraction. The app name and favicon appear in the panel header instantly — no manual selection needed for recognised apps.
Coverage grid loads Core UX
Shows which page types are already captured (green ✓) and which are still missing (gray chips). A count shows "12 / 47" at a glance. This answers the question "what do I actually still need to capture?" before you do anything else.
Click a missing page type chip
Pre-selects that page type in the capture flow. The capture mode buttons pulse once with a CSS animation — a single attention signal to show readiness without being intrusive.
Choose a capture mode
Viewport, Section, Element, or Full Page — each suited to a different kind of screen. The choice is made once; the extension handles the rest.
Preview appears
The page type is already filled in from step 3. Title follows the library's naming convention automatically. The only decision left is "does this look right?" — confirm or recapture.
Upload & Create Post — one click
The background service worker sends the image and metadata to the WordPress REST API endpoint (saasboat/v1/capture), which creates the media attachment and the boat_screenshot post in a single request.
Success — next target suggested Assistant feel
Coverage count updates immediately (optimistic state — no re-fetch). The "Capture Another" button becomes "Next: Empty State →" — the alphabetically first remaining missing page type. The panel already knows what you should do next.
The Coverage Panel — The Core UX Idea
Most capture tools ask: "what do you want to capture?" The coverage panel inverts this: "here's what you haven't captured yet." That's not a small shift. It changes the tool from a clipboard to an assistant.
Why coverage-first changes the workflow
Without coverage awareness, a capture session goes like this: open the app, take a screenshot, try to remember if you already have this page type, check WordPress, come back. With the coverage panel, you open the panel and it tells you exactly what's still missing before you've touched anything.
The coverage count ("12 / 47") also creates a progress mechanic. It makes systematic library-building feel like completing a checklist, not performing an open-ended chore.
Chips update optimistically after each upload — the UI doesn't re-fetch. The chip that was just captured flips from gray to green immediately. The count increments. This responsiveness is what makes the tool feel fast rather than connected to a slow backend.
The Four Capture Modes
Viewport
Captures exactly what's visible in the browser window. Simplest mode, used for the majority of screenshots.
document.documentElement.clientWidth (not window.innerWidth) as crop width — this excludes the scrollbar so it never appears in the captured image. A small detail that matters at 3,661 screenshots.Section
Freehand rectangle selection. Drag to draw the box over any region of the page.
Element
Click any DOM element to capture its exact bounding box via getBoundingClientRect().
Full Page
Scrolls the page in viewport-height increments and stitches all slices into a single tall canvas.
fixed and sticky elements are hidden so nav bars don't repeat across slices. Scale is derived from actual image pixel dimensions (not devicePixelRatio) — this prevents misaligned stitches when captureVisibleTab doesn't capture at the reported DPR.App Detection
On every tab load or navigation, the extension runs a two-pass detection routine to identify which SaaS app is currently open — so the user never has to type an app name.
Exact hostname match
Tries to match the current tab's hostname directly against the _boat_app_website field stored in WordPress for each app. app.notion.so matches notion.so. Fast, accurate, zero ambiguity for recognised apps.
Brand token extraction (fallback)
Strips known generic subdomains — app, my, www, dashboard, login, auth, portal, staging, beta — from the hostname, then matches the remaining tokens against app slugs and names. my.slack.com → strips my → matches slack. app.notion.so → strips app → matches notion.
Favicon from Google CDN
The matched app's favicon is fetched from favicon?domain=…&sz=64 — no extra API call, always current, always the right branding. Manual dropdown fallback for unrecognised apps, showing the full sorted list from the cache.
Hide Elements
Before capturing, any element on the page can be removed from the screenshot — cookie banners, live chat widgets, development overlays, floating CTAs. The mode is designed for repeated use across multiple capture passes without re-hiding.
Click to hide, Shift+click to continue
Single click: hides the element (display: none !important) and exits hide mode. Shift+click: hides and stays in mode — for removing multiple elements in sequence. Clicking an already-hidden element (shown with green overlay) restores it.
Undo + persistent state
An Undo button in the panel restores the last hidden element one at a time. Hidden state persists across captures — hide once, then take Viewport, Section, and Element shots of the same page without rehiding.
Hover uses brand blue (#0996F0)
Elements being highlighted on hover use the SaaS Boat brand blue. This keeps the interaction looking native to the product rather than generic.
About-to-hide switches to red
When a click is about to hide an element (removing it from view), the overlay switches to red (#e74c3c). A visual signal that this interaction has a consequence — it changes what appears in the screenshot.
Paste & Upload Bypass
For cases where an external tool produces a better result — a different crop, a Figma export, a higher-fidelity capture — the panel accepts images from three non-capture paths.
Ctrl+V paste
Paste an image from clipboard anywhere in the panel. Calls navigator.clipboard.read(). Clipboard images are normalised to PNG via canvas — consistent format regardless of whether the clipboard contains JPEG, WebP, or BMP.
File upload
Opens a native file picker. Bypasses all canvas processing entirely — the file is sent byte-for-byte to WordPress, preserving original quality exactly. Best path for Figma exports and high-fidelity source files.
Drops into the same preview flow
All three bypass paths land in the same preview screen as a native capture — page type pre-filled, same one-click upload. The source of the image doesn't change what happens next.
Architecture
Three layers with a distinct reason for each — not "clean code" for its own sake, but three actual constraints that made separation necessary.
chrome.storage.local (cached app/page-type data), relays commands to content.js via chrome.tabs.sendMessage, proxies API requests to background.js via chrome.runtime.sendMessage.saasboat/v1/capture) accepts a base64 image and creates the media attachment + boat_screenshot post in one request. The standard WP REST API requires two separate calls (upload media → create post). One custom endpoint eliminates a round trip and a failure surface.Why cache app data? App and page-type data is cached in
chrome.storage.local and synced on install, startup, or manual refresh. The panel loads instantly without waiting for an API call on every open — a critical UX decision when you're opening the panel dozens of times per session.
Technical Challenges Solved
Full-page stitching creates visible seams between slices when scroll position isn't exactly aligned with slice boundaries.
Scale is derived from actual image pixel height, not devicePixelRatio. Each slice is drawn at its actual scrollY position. 1–2px inaccuracies create slight overlaps rather than gaps — later slices overwrite earlier ones.
Sticky headers and nav bars repeat across every slice in a full-page capture.
Capture the first slice normally. Before each subsequent slice, hide all fixed and sticky elements with display:none. After the last slice, restore all of them. A double requestAnimationFrame wait ensures the DOM repaint completes before capture fires.
Scrollbar pixels appear in screenshots, creating a narrow strip of UI chrome that shouldn't be there.
Use document.documentElement.clientWidth as the crop width. Unlike window.innerWidth, this excludes the scrollbar. A one-line fix that applies consistently across all 3,661 screenshots.
Browsers strip Authorization headers on cross-origin requests made from extension page contexts.
All API calls are routed through the background service worker, which doesn't have this restriction. The side panel never makes API calls directly — it sends messages to background.js, which makes the authenticated request and returns the result.
CORS blocks pagination headers — WordPress returns X-WP-TotalPages but extension contexts can't read it across origins.
Paginate via loop-until-empty: keep fetching the next page until the response array comes back empty. No dependency on a header that can't be read — works regardless of CORS policy.
Element capture spanning multiple viewport heights requires stitching while keeping track of which part of each slice belongs to the target element.
A scroll-and-stitch loop bounded to the element's document-relative dimensions. Each iteration calculates srcY — the offset within that slice that corresponds to the element's bounds — and draws only that portion to the output canvas.
Panel States & Design Language
Eight distinct states managed as a pure CSS class toggle system — no frameworks, no virtual DOM. Each state is a <div class="panel"> that gains the active class. Transitions are CSS only.
The design language is the SaaS Boat design system applied to a 360px-wide panel context. Everything that looks intentional on the main site also looks intentional inside the extension.
Hint bars injected into pages use Manrope, the #161514 near-black background, and 100px border radius — they look like they belong to the SaaS Boat brand, not a generic Chrome extension popup.
Outcomes
Learnings
The best internal tool design looks like product design. The coverage panel, the "next missing" suggestion, the optimistic updates — these are product decisions, not developer conveniences. Internal tools often have zero UX consideration because "it's just for us." Applying the same design thinking here is what made it fast enough to actually use at scale.
Coverage-first inverts the interaction model. Asking "what should I capture?" puts the cognitive load on the user. Showing "here's what's missing" removes it. The distinction sounds small but it's the difference between a tool that requires discipline to use systematically and one that creates the systematic workflow automatically.
Architecture decisions are UX decisions. Routing all API calls through the service worker isn't just a technical choice — it's what makes the extension reliable enough to trust in a production workflow. The optimistic coverage update isn't just a performance trick — it's what makes the tool feel responsive rather than laggy. Every architectural decision has a user-facing consequence.
Edge cases at scale become visible. The scrollbar pixel issue, the sticky-header repetition, the DPR stitching misalignment — none of these matter if you take 10 screenshots. At 3,661, they matter a lot. Designing for the real scale of use revealed problems that a prototype at "a few screenshots" would never surface.
The tool is the proof of the product thinking. Anyone looking at SaaS Boat might wonder how 3,661 screenshots got into the library. The answer — a purpose-built Chrome extension with coverage tracking, automatic app detection, four capture modes, and a one-click upload — is itself a demonstration of how I think about the full product stack, not just the UI layer.