<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Robert Wallace</title>
    <description>The latest articles on DEV Community by Robert Wallace (@utilitylab).</description>
    <link>https://dev.to/utilitylab</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4006231%2Fa3ef09aa-df38-4f29-9879-e6b872f274ea.jpg</url>
      <title>DEV Community: Robert Wallace</title>
      <link>https://dev.to/utilitylab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/utilitylab"/>
    <language>en</language>
    <item>
      <title>How to Animate SVG Without JavaScript (Pure CSS Animation)</title>
      <dc:creator>Robert Wallace</dc:creator>
      <pubDate>Mon, 06 Jul 2026 06:44:56 +0000</pubDate>
      <link>https://dev.to/utilitylab/how-to-animate-svg-without-javascript-pure-css-animation-3dc4</link>
      <guid>https://dev.to/utilitylab/how-to-animate-svg-without-javascript-pure-css-animation-3dc4</guid>
      <description>&lt;p&gt;`Every time I see a developer reach for a JavaScript library just to animate an SVG icon, I die a little inside. A spinning loader? A draw-in logo? A hover effect on an illustration? You don't need GreenSock. You don't need Anime.js. You don't need a single line of JavaScript.&lt;/p&gt;

&lt;p&gt;CSS can animate SVG directly. It's faster, lighter, and simpler than pulling in a dependency. Here's how to do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Foundation: SVG and CSS Working Together
&lt;/h2&gt;

&lt;p&gt;SVG elements in the DOM are just elements — which means CSS properties like &lt;code&gt;transform&lt;/code&gt;, &lt;code&gt;opacity&lt;/code&gt;, and custom &lt;code&gt;stroke-*&lt;/code&gt; properties apply to them. The key difference from animating HTML elements is that SVG uses its own coordinate system for transforms (centered on the SVG viewBox, not the element's center), and it has special properties for line drawing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 1: Draw-In Effects with &lt;code&gt;stroke-dasharray&lt;/code&gt; and &lt;code&gt;stroke-dashoffset&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is the most satisfying SVG animation technique. It makes it look like an artwork is being drawn by an invisible pen.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`css&lt;br&gt;
.path {&lt;br&gt;
  stroke-dasharray: 1000;&lt;br&gt;
  stroke-dashoffset: 1000;&lt;br&gt;
  animation: draw 2s ease-in-out forwards;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;@keyframes draw {&lt;br&gt;
  to {&lt;br&gt;
    stroke-dashoffset: 0;&lt;br&gt;
  }&lt;br&gt;
}`&lt;/p&gt;

&lt;p&gt;How it works: stroke-dasharray creates a dashed line. When the dash length equals the total path length, the line is solid. stroke-dashoffset slides that dash along the path. Start with the offset equal to the path length (the "dash" is shifted completely out of view), then animate it to 0. The line appears to draw itself.&lt;/p&gt;

&lt;p&gt;Pro tip: You need the exact path length for this to work. Use path.getTotalLength() in your browser's DevTools console to find it, then set both stroke-dasharray and stroke-dashoffset to that value. Or just use a generously large value like 1000 — it works as long as both properties match.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 2:  CSS Transforms for Scaling and Rotation
&lt;/h2&gt;

&lt;p&gt;SVG elements respond to transform just like HTML elements, but with one gotcha: the transform origin defaults to (0, 0) of the SVG viewBox, not the center of the element.&lt;/p&gt;

&lt;p&gt;`.gear {&lt;br&gt;
  animation: spin 4s linear infinite;&lt;br&gt;
  transform-origin: center;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;@keyframes spin {&lt;br&gt;
  to {&lt;br&gt;
    transform: rotate(360deg);&lt;br&gt;
  }&lt;br&gt;
}`&lt;/p&gt;

&lt;p&gt;For complex grouped SVGs, you may need to set transform-origin explicitly to the center of the group's bounding box. Use percentage values or specific pixel coordinates.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.gear-group {&lt;br&gt;
  transform-origin: 120px 120px;&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 3:  Staggered Animations with animation-delay
&lt;/h2&gt;

&lt;p&gt;Multiple elements animating at the same time looks mechanical. Add delays to create a cascade effect.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.logo-letter:nth-child(1) { animation-delay: 0s; }&lt;br&gt;
.logo-letter:nth-child(2) { animation-delay: 0.1s; }&lt;br&gt;
.logo-letter:nth-child(3) { animation-delay: 0.2s; }&lt;br&gt;
.logo-letter:nth-child(4) { animation-delay: 0.3s; }&lt;br&gt;
.logo-letter:nth-child(5) { animation-delay: 0.4s; }&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Combine this with animation-fill-mode: backwards so elements stay invisible before their animation starts, and you get a polished reveal sequence with zero JavaScript.&lt;/p&gt;

&lt;p&gt;Performance: CSS vs. JavaScript&lt;br&gt;
For simple SVG animations (transforms, opacity, stroke-draw), CSS animations are faster than JavaScript libraries. Here's why:&lt;/p&gt;

&lt;p&gt;CSS animations run on the compositor thread, not the main thread. JavaScript runs on the main thread. If your page is doing other work (network requests, layout calculations), JS animations hitch. CSS animations don't.&lt;br&gt;
CSS animations have zero bundle cost. GreenSock is ~40KB minified. Anime.js is ~15KB. Even a few lines of custom JS requestAnimationFrame logic adds maintenance cost. CSS keyframes are free.&lt;br&gt;
The GPU can accelerate CSS transform and opacity animations without involving the CPU. JS libraries can trigger GPU compositing, but they add overhead.&lt;br&gt;
The one place JS wins: complex timeline sequencing, physics-based motion (springs, easing curves you can't express in CSS), and path morphing across multiple shapes. But for 90% of real-world SVG animation needs — loaders, hovers, draw-in effects, reveals — CSS is the better tool.&lt;/p&gt;

&lt;p&gt;Real Scenario: A Loading Spinner and an Animated Logo&lt;br&gt;
Loading Spinner (no JS, no images)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;svg viewBox="0 0 50 50" class="spinner"&amp;gt;&lt;br&gt;
  &amp;lt;circle cx="25" cy="25" r="20" fill="none" stroke="#6366f1" stroke-width="4" /&amp;gt;&lt;br&gt;
&amp;lt;/svg&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;`.spinner circle {&lt;br&gt;
  stroke-dasharray: 126;&lt;br&gt;
  stroke-dashoffset: 126;&lt;br&gt;
  transform-origin: center;&lt;br&gt;
  animation: spin 1.2s linear infinite;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;@keyframes spin {&lt;br&gt;
  to {&lt;br&gt;
    stroke-dashoffset: 0;&lt;br&gt;
    transform: rotate(360deg);&lt;br&gt;
  }&lt;br&gt;
}`&lt;/p&gt;

&lt;p&gt;One element, two properties, zero JS. Works in every modern browser.&lt;/p&gt;

&lt;p&gt;Animated Logo Draw-In&lt;br&gt;
A 5-letter wordmark where each letter draws in sequence:&lt;/p&gt;

&lt;p&gt;`.wordmark path {&lt;br&gt;
  stroke-dasharray: 500;&lt;br&gt;
  stroke-dashoffset: 500;&lt;br&gt;
  animation: draw 0.8s ease-out forwards;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;.wordmark path:nth-child(1) { animation-delay: 0s; }&lt;br&gt;
.wordmark path:nth-child(2) { animation-delay: 0.15s; }&lt;br&gt;
.wordmark path:nth-child(3) { animation-delay: 0.3s; }&lt;br&gt;
.wordmark path:nth-child(4) { animation-delay: 0.45s; }&lt;br&gt;
.wordmark path:nth-child(5) { animation-delay: 0.6s; }&lt;/p&gt;

&lt;p&gt;@keyframes draw {&lt;br&gt;
  to { stroke-dashoffset: 0; }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;The letters appear one after another, each drawing itself from left to right. Clean, performant, and entirely CSS-driven.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Easy Way
&lt;/h2&gt;

&lt;p&gt;You don't need to write all this CSS by hand for every project. I built an SVG animation playground where you can upload any SVG, experiment with draw-in, spin, fade, and stagger effects visually, and export the CSS code — no login, no uploads, no JavaScript.&lt;/p&gt;




&lt;p&gt;I build lightweight browser-based tools at utilitylab.dev. If you want a free, no-login tool to animate SVGs with pure CSS and export the code, there's one at &lt;a href="https://utilitylab.dev/svg-animator" rel="noopener noreferrer"&gt;&amp;lt;https://utilitylab.dev/svg-animator&lt;/a&gt;&amp;gt;.&lt;/p&gt;

</description>
      <category>animation</category>
      <category>design</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Split SRT Subtitle Files for TikTok, Shorts, and Course Clips</title>
      <dc:creator>Robert Wallace</dc:creator>
      <pubDate>Mon, 06 Jul 2026 06:29:06 +0000</pubDate>
      <link>https://dev.to/utilitylab/how-to-split-srt-subtitle-files-for-tiktok-shorts-and-course-clips-1lbi</link>
      <guid>https://dev.to/utilitylab/how-to-split-srt-subtitle-files-for-tiktok-shorts-and-course-clips-1lbi</guid>
      <description>&lt;p&gt;`You just finished recording a 90-minute podcast. It's gold — packed with quotable moments, sharp insights, funny tangents. You want to repurpose it into ten 60-second Shorts. You've got the video clips cut. Now you need the subtitles to match.&lt;/p&gt;

&lt;p&gt;If you've ever tried to cut a long video into short clips and keep the subtitles aligned, you know exactly how painful this gets. The original SRT file covers the full 90 minutes. Each clip needs a separate SRT with remapped timestamps. Doing it by hand means opening the file, scanning through thousands of cue lines, copying the right block, and manually adjusting every timestamp.&lt;/p&gt;

&lt;p&gt;There's a better way. Here's everything you need to know about splitting SRT files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why You Need to Split Subtitles
&lt;/h2&gt;

&lt;p&gt;SRT splitting comes up more often than you'd think:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Long-form to short-form repurposing&lt;/strong&gt;: A 2-hour podcast becomes 20 TikTok clips. Each clip needs its own subtitle file with timestamps starting at 00:00.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Course modules&lt;/strong&gt;: A 60-minute lecture gets split into 10 six-minute lessons. Each module needs accurate subtitles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interview highlights&lt;/strong&gt;: A recorded interview yields 5–8 highlight clips for social. You need captions that match each excerpt exactly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-platform delivery&lt;/strong&gt;: Your video lives on YouTube with full captions, but the 15-second clips for Reels need separate subtitle files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without proper splitting, you end up with subtitles that say the wrong thing at the wrong time — or, worse, cues that reference times far outside the clip duration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Common Problems (And How to Fix Them)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Misaligned Timestamps
&lt;/h3&gt;

&lt;p&gt;When you pull a segment from &lt;code&gt;00:12:34,567&lt;/code&gt; to &lt;code&gt;00:13:20,891&lt;/code&gt; and paste it into a new file without adjusting, the cues still reference the original timecodes. Your first subtitle might say &lt;code&gt;00:12:34,567 --&amp;gt; 00:12:36,000&lt;/code&gt; — which means nothing in a 60-second clip that starts at 00:00.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: Subtract the segment start time from every timestamp. If your clip starts at &lt;code&gt;00:12:34,567&lt;/code&gt;, every &lt;code&gt;start_time&lt;/code&gt; and &lt;code&gt;end_time&lt;/code&gt; in that block needs &lt;code&gt;00:12:34,567&lt;/code&gt; subtracted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Missing Cues
&lt;/h3&gt;

&lt;p&gt;You copy what you think is the right range, but you accidentally skip a cue. The clip plays and a sentence has no subtitle for 3 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: Always verify that your cue count matches. A 60-second clip of dialogue should have roughly 8–15 cues depending on speaking speed. If the count seems low, go back and check you didn't miss any.&lt;/p&gt;

&lt;h3&gt;
  
  
  Encoding Issues
&lt;/h3&gt;

&lt;p&gt;SRT files are plain text, but they need to be UTF-8 encoded. If you edit an SRT in Notepad on Windows and save it as ANSI, special characters — accented letters, em dashes, smart quotes — turn into garbage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: Always save SRT files with UTF-8 encoding, no BOM. Most video editors will handle UTF-8 correctly. ANSI or UTF-16? Those cause silent failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Ways to Split an SRT File
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. By Time Range
&lt;/h3&gt;

&lt;p&gt;This is the most common approach: define a start and end time, then extract every cue whose timestamp falls within that window. The tool remaps the start/end times so the first cue begins at &lt;code&gt;00:00:00,000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Use this when you've already cut your video and know the exact timecodes for each clip.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. By Cue Count
&lt;/h3&gt;

&lt;p&gt;Set a number of cues per split (e.g., 20 cues per file). The tool cuts at cue boundaries automatically.&lt;/p&gt;

&lt;p&gt;Use this when you're splitting a long subtitle file into equal-size chunks — great for chapter-based courses where each module should have roughly the same number of captions.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. By Chapter Markers
&lt;/h3&gt;

&lt;p&gt;Some subtitle files include chapter markers or scene changes. If your source material has clear breaks, you can split at those points.&lt;/p&gt;

&lt;p&gt;Use this when your content is already organized into sections (like a conference talk with defined parts or an interview segmented by topic).&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Scenario: A 2-Hour Conference Talk → 12 Instagram Reels
&lt;/h2&gt;

&lt;p&gt;A client delivered a 2-hour conference keynote. They wanted 12 Reels — one per topic segment — each 45–90 seconds long.&lt;/p&gt;

&lt;p&gt;The original SRT had 340 cues. Here's the workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Mark the segments&lt;/strong&gt;: Watch the video and note the timecodes for each topic. Segment 1: &lt;code&gt;00:00:00&lt;/code&gt; to &lt;code&gt;00:08:15&lt;/code&gt;. Segment 2: &lt;code&gt;00:08:15&lt;/code&gt; to &lt;code&gt;00:16:40&lt;/code&gt;. And so on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extract by time range&lt;/strong&gt;: For each segment, pull all cues within the time window. The tool remaps timestamps so each clip's first cue starts at 00:00.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify cue alignment&lt;/strong&gt;: Play each clip with its subtitle file. Check that the spoken words match the on-screen text. Pay extra attention at cut points — sometimes a sentence straddles two segments and the half-sentence in the next clip is confusing without context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export&lt;/strong&gt;: Save each SRT with a matching filename. &lt;code&gt;clip-01.srt&lt;/code&gt;, &lt;code&gt;clip-02.srt&lt;/code&gt;, etc. Most editors auto-detect the subtitle file if it shares the video's base name.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole process took about 20 minutes — down from what would have been hours of manual timestamp editing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Easy Way
&lt;/h2&gt;

&lt;p&gt;You can do this with a text editor and a spreadsheet. Or you can use a tool purpose-built for the job. I built an SRT splitter that does all of this in your browser — paste your SRT, choose your split method (by time, by cue count, or by chapter), and download the split files instantly. No login, no upload, no server processing.&lt;/p&gt;




&lt;p&gt;I build lightweight browser-based tools at utilitylab.dev. If you want a free, no-login tool to split SRT subtitle files for TikTok, Shorts, or course clips, there's one at &lt;a href="https://utilitylab.dev/srt-splitter" rel="noopener noreferrer"&gt;https://utilitylab.dev/srt-splitter&lt;/a&gt;.`&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>tutorial</category>
      <category>tooling</category>
    </item>
    <item>
      <title>How to Calculate Title-Safe and Action-Safe Margins for Video</title>
      <dc:creator>Robert Wallace</dc:creator>
      <pubDate>Mon, 06 Jul 2026 06:19:58 +0000</pubDate>
      <link>https://dev.to/utilitylab/how-to-calculate-title-safe-and-action-safe-margins-for-video-1ifj</link>
      <guid>https://dev.to/utilitylab/how-to-calculate-title-safe-and-action-safe-margins-for-video-1ifj</guid>
      <description>&lt;p&gt;`You export a video, upload it to a platform, and — every time — text gets chopped at the edges. Maybe it's a lower-third title that looks centered in Premiere but gets clipped on YouTube. Maybe it's a call-to-action banner that's invisible on a TV broadcast. Or maybe you designed a beautiful motion graphics package only to find key elements falling off the sides on Instagram Reels.&lt;/p&gt;

&lt;p&gt;The culprit? Safe zones — or more precisely, the lack of them.&lt;/p&gt;

&lt;p&gt;If you're delivering video to any platform beyond your own laptop, you need to understand title-safe and action-safe margins. Here's what they are, why they matter, and how to calculate them for any format.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Title-Safe and Action-Safe Areas?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Action-safe&lt;/strong&gt; is the outer boundary. It typically sits at 90% of the frame (a 5% margin on each edge). Anything important happening visually — people, movement, scene composition — should stay within this area. Elements outside it risk being cropped by overscan on older TVs or by platform-specific cropping on social media.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Title-safe&lt;/strong&gt; is the inner boundary. It sits at 80% of the frame (a 10% margin on each edge). Any text, logos, or critical UI elements belong here. This gives you a generous buffer even on aggressively cropped displays.&lt;/p&gt;

&lt;p&gt;These aren't arbitrary numbers. They come from broadcast television standards (SMPTE RP 27.3 and ITU-R BT.1973), where CRT overscan could eat 5–10% of the visible picture. The standards carried over into digital production — and they're still relevant today because every platform crops differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calculating Safe Zones for Any Aspect Ratio
&lt;/h2&gt;

&lt;p&gt;The math is straightforward once you know your frame dimensions.&lt;/p&gt;

&lt;h3&gt;
  
  
  16:9 (1920 × 1080)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Zone&lt;/th&gt;
&lt;th&gt;Margin&lt;/th&gt;
&lt;th&gt;Pixel bounds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full frame&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;1920 × 1080&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action-safe&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;td&gt;1728 × 972 (centered)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Title-safe&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;1536 × 864 (centered)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To calculate: subtract &lt;code&gt;2 × margin_percentage × dimension&lt;/code&gt; from each side. For a 10% margin on 1920px wide: &lt;code&gt;1920 - (2 × 0.10 × 1920) = 1536&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4:3 (1440 × 1080)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Zone&lt;/th&gt;
&lt;th&gt;Margin&lt;/th&gt;
&lt;th&gt;Pixel bounds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full frame&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;1440 × 1080&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action-safe&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;td&gt;1296 × 972&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Title-safe&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;1152 × 864&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Vertical 9:16 (1080 × 1920) — for TikTok, Reels, Shorts
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Zone&lt;/th&gt;
&lt;th&gt;Margin&lt;/th&gt;
&lt;th&gt;Pixel bounds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full frame&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;1080 × 1920&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action-safe&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;td&gt;972 × 1728&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Title-safe&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;864 × 1536&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The same percentages work across ratios — just plug in your dimensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Platform Matters
&lt;/h2&gt;

&lt;p&gt;Here's the messy reality: the 5%/10% rule is a &lt;em&gt;guide&lt;/em&gt;, not a guarantee. Different platforms crop differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TV broadcast&lt;/strong&gt; (NTSC/PAL): Overscan can eat 5–10%. Use strict 80/90% guides.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;YouTube&lt;/strong&gt;: Uses letterboxing/pillarboxing but doesn't overscan. Still, the YouTube player UI (progress bar, buttons) overlays on your video — keep text out of the bottom 8%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instagram Reels / TikTok&lt;/strong&gt;: These platforms add their own UI chrome over your video. TikTok's caption bar, like button, and comment button overlap the bottom and right edges. Keep critical text inside the center 80%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Digital signage&lt;/strong&gt;: Custom aspect ratios (32:9 ultrawide, 2:1, even 1:1 if displayed on square screens). Always ask for the exact display resolution before designing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real Scenario: Delivering to Both Broadcast and Reels
&lt;/h2&gt;

&lt;p&gt;A client needs motion graphics for a TV commercial &lt;em&gt;and&lt;/em&gt; 9:16 Reels cutdowns from the same 1920 × 1080 master.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The master&lt;/strong&gt; is framed with a 10% title-safe zone (1536 × 864). The logo bug sits in the bottom-right corner at x=1728, y=972 — inside action-safe but outside title-safe. That works for broadcast because the logo is visual, not text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Reels cutdown&lt;/strong&gt; crops the center 1080 × 1920 region of the 1920 × 1080 master. The logo at 1728,972? Completely gone — it falls outside the 1080px width. Lesson: design text for the &lt;em&gt;tightest&lt;/em&gt; format first. In this case, place all text inside a 864 × 1200 safe zone at the center — that survives both broadcast overscan and the Reels center crop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Easy Way
&lt;/h2&gt;

&lt;p&gt;You can do this math by hand every time, or you can use a tool that does it for you. I built an interactive safe-zone calculator that works in your browser — no login required, no cloud processing. You pick your resolution and aspect ratio, and it draws the guides instantly on a preview canvas.&lt;/p&gt;




&lt;p&gt;I build lightweight browser-based tools at utilitylab.dev. If you want a free, no-login tool to calculate title-safe and action-safe margins for any video format, there's one at &lt;a href="https://utilitylab.dev/safe-zone-generator" rel="noopener noreferrer"&gt;https://utilitylab.dev/safe-zone-generator&lt;/a&gt;.`&lt;/p&gt;

</description>
      <category>design</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>tooling</category>
    </item>
    <item>
      <title>How to Share Large Files Browser-to-Browser Without Upload Servers</title>
      <dc:creator>Robert Wallace</dc:creator>
      <pubDate>Mon, 06 Jul 2026 06:08:21 +0000</pubDate>
      <link>https://dev.to/utilitylab/how-to-share-large-files-browser-to-browser-without-upload-servers-5b6i</link>
      <guid>https://dev.to/utilitylab/how-to-share-large-files-browser-to-browser-without-upload-servers-5b6i</guid>
      <description>&lt;p&gt;`You need to send a 4 GB video file to a designer on the other side of the office. What do you do?&lt;/p&gt;

&lt;p&gt;If you're like most people, you fire up WeTransfer, Dropbox, or Google Drive. You wait for the upload. They wait for the download. The file sits on someone else's server for 7 days, then it's gone. And if your file is over 2 GB, some of those services just say no.&lt;/p&gt;

&lt;p&gt;There's a better way: send the file directly from your browser to theirs, with zero bytes touching a server.&lt;/p&gt;

&lt;h2&gt;
  
  
  How WebRTC File Transfers Actually Work
&lt;/h2&gt;

&lt;p&gt;WebRTC (Web Real-Time Communication) is the same technology that powers video calls in Zoom, Google Meet, and Discord. But it also handles arbitrary data — including large files — through its &lt;code&gt;RTCDataChannel&lt;/code&gt; API.&lt;/p&gt;

&lt;p&gt;Here's the flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Signaling.&lt;/strong&gt; Your browser creates an "offer" containing your connection capabilities (codecs, network details). This offer is sent to the recipient through a lightweight signaling server — typically just a WebSocket or a simple HTTP relay. The signaling server never sees the file data; it only exchanges connection metadata.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ICE negotiation.&lt;/strong&gt; Both browsers exchange ICE (Interactive Connectivity Establishment) candidates. These are potential routes for the direct connection: same-network LAN, public IP via STUN, or relayed via TURN if both peers are behind strict NATs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Direct peer connection.&lt;/strong&gt; Once a route is found, the browsers connect directly. From this point forward, data flows peer-to-peer. The signaling server's job is done.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data channel transfer.&lt;/strong&gt; The file is chunked into small pieces (typically 16 KB each, respecting the SCTP MTU), transferred over the encrypted data channel, and reassembled on the receiving end. Progress is tracked in real time.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The entire transfer uses &lt;strong&gt;DTLS encryption&lt;/strong&gt; — the same encryption layer that secures HTTPS. Nobody on the network can read the data in transit, and the signaling server never touches the payload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why No-Upload Is More Private
&lt;/h2&gt;

&lt;p&gt;When you upload a file to WeTransfer or Dropbox, you're trusting that company with your data. Even with "encryption at rest," the company holds the keys. Their terms of service typically grant them broad rights to scan, analyze, or share your content for abuse prevention, legal compliance, or even product improvement.&lt;/p&gt;

&lt;p&gt;With a direct browser-to-browser transfer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The file never exists on any server (except the signaling metadata, which is discarded seconds after the connection is established)&lt;/li&gt;
&lt;li&gt;The transfer is end-to-end encrypted by WebRTC's DTLS — no middleman can decrypt it&lt;/li&gt;
&lt;li&gt;No account or login is required on either side&lt;/li&gt;
&lt;li&gt;No copy lingers on a cloud provider's storage after the transfer completes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For sensitive assets — design mockups for an unreleased product, legal documents, internal financial reports — this is a meaningful privacy improvement over "upload to cloud and share a link."&lt;/p&gt;

&lt;h2&gt;
  
  
  When P2P File Sharing Works Best
&lt;/h2&gt;

&lt;p&gt;Direct transfers shine in specific scenarios:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same local network.&lt;/strong&gt; Two people in the same office building can transfer files at LAN speeds (500 Mbps to 1 Gbps) instead of being bottlenecked by the building's internet uplink. A 5 GB file that takes 15 minutes over WeTransfer might take 30 seconds over the LAN.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same-office asset handoff.&lt;/strong&gt; Designers sending PSDs to developers, video editors passing RAW footage to colorists, engineers sharing log dumps — any scenario where both parties are online and need the file now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Internal team tools.&lt;/strong&gt; Instead of maintaining an internal file-drop server, teams can use a P2P transfer page that requires zero infrastructure. No S3 buckets, no file servers, no expiring-link cleanup scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Size Limits
&lt;/h2&gt;

&lt;p&gt;The real limit isn't your internet connection — it's your browser's memory.&lt;/p&gt;

&lt;p&gt;WebRTC data channels stream data in chunks, and the receiving browser needs to reassemble the file in memory. Chrome and Firefox can typically handle files up to 2 GB before memory pressure becomes noticeable. For larger files, look for a tool that uses the File System Access API to stream chunks directly to disk.&lt;/p&gt;

&lt;p&gt;Network speed matters too. A 10 GB file over a symmetric gigabit connection takes about 80 seconds. Over a typical residential uplink (20 Mbps), that same file takes nearly 70 minutes — and both browsers need to stay open the whole time.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to Use P2P File Sharing
&lt;/h2&gt;

&lt;p&gt;Direct transfers aren't the answer for everything. Skip them when:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Asynchronous transfers.&lt;/strong&gt; The recipient needs to download the file tomorrow, but you want to send it now. P2P requires both parties to be online simultaneously. If the recipient closes their browser, the transfer fails. For async, you still need a server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mobile browsers.&lt;/strong&gt; Mobile Safari and Chrome aggressively throttle background tabs and WebRTC data channels. A transfer that works fine on desktop may fail or stall on a phone, especially for files over 100 MB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strict corporate networks.&lt;/strong&gt; Some enterprise firewalls block the UDP ports that WebRTC uses for ICE negotiation. The connection may fall back to a TURN relay, which re-introduces a server hop (and potential bandwidth limits).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Large groups.&lt;/strong&gt; Sending a file to 50 people? P2P means 50 separate upload streams from your browser. A traditional upload-to-server-and-share model is far more efficient here.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Browser-to-browser file sharing isn't a replacement for cloud storage — it's a complement. For the specific case where both parties are online, the file is too large for WeTransfer's cap, or privacy matters more than convenience, P2P transfer is faster, more private, and simpler to set up than any server-based alternative.&lt;/p&gt;




&lt;p&gt;I build lightweight browser-based tools at &lt;strong&gt;utilitylab.dev&lt;/strong&gt;. If you want a free, no-login tool to share files directly browser-to-browser without uploading to a server, there's one at &lt;a href="https://utilitylab.dev/p2p-file-share" rel="noopener noreferrer"&gt;https://utilitylab.dev/p2p-file-share&lt;/a&gt;.`&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>privacy</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Add JSON-LD Schema for AI Search Engines (OpenAI, Perplexity, Claude)</title>
      <dc:creator>Robert Wallace</dc:creator>
      <pubDate>Mon, 06 Jul 2026 05:12:07 +0000</pubDate>
      <link>https://dev.to/utilitylab/how-to-add-json-ld-schema-for-ai-search-engines-openai-perplexity-claude-2olj</link>
      <guid>https://dev.to/utilitylab/how-to-add-json-ld-schema-for-ai-search-engines-openai-perplexity-claude-2olj</guid>
      <description>&lt;p&gt;`You've got perfect meta descriptions, proper Open Graph tags, and a sitemap that Googlebot loves. So why does ChatGPT Search return outdated info about your app, and why does Perplexity hallucinate your pricing?&lt;/p&gt;

&lt;p&gt;Because AI search engines don't parse the web the way Google does. They extract structured data into their reasoning context — and if your JSON-LD is missing, incomplete, or loosely typed, the AI fills gaps with whatever it &lt;em&gt;thinks&lt;/em&gt; is correct. That's how "probably around $20/month" becomes a confident lie in a search result.&lt;/p&gt;

&lt;h2&gt;
  
  
  What JSON-LD Actually Does for AI
&lt;/h2&gt;

&lt;p&gt;JSON-LD (JavaScript Object Notation for Linked Data) is a way to embed structured metadata in your HTML. Traditional search engines use it for rich snippets. AI search engines use it as &lt;strong&gt;factual grounding&lt;/strong&gt; — they pull your schema into their context window instead of guessing from prose.&lt;/p&gt;

&lt;p&gt;OpenAI Search, Perplexity, and Claude-powered search all read JSON-LD &lt;code&gt;&amp;lt;script type="application/ld+json"&amp;gt;&lt;/code&gt; blocks. Get it right, and the AI cites your exact features, pricing, and availability. Get it wrong or miss it, and the AI infers whatever it can from body text — including your competitor's data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Schema Types That Matter for AI
&lt;/h2&gt;

&lt;p&gt;Not all schema types are created equal. For AI extraction, these perform best:&lt;/p&gt;

&lt;h3&gt;
  
  
  SoftwareApplication
&lt;/h3&gt;

&lt;p&gt;Perfect for SaaS tools and apps. Include &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;applicationCategory&lt;/code&gt;, &lt;code&gt;operatingSystem&lt;/code&gt;, &lt;code&gt;offers&lt;/code&gt; (with &lt;code&gt;price&lt;/code&gt; and &lt;code&gt;priceCurrency&lt;/code&gt;), and &lt;code&gt;featureList&lt;/code&gt;. AI models are trained to pull &lt;code&gt;applicationCategory&lt;/code&gt; directly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`json&lt;br&gt;
{&lt;br&gt;
  "&lt;a class="mentioned-user" href="https://dev.to/context"&gt;@context&lt;/a&gt;": "&lt;a href="https://schema.org" rel="noopener noreferrer"&gt;https://schema.org&lt;/a&gt;",&lt;br&gt;
  "@type": "SoftwareApplication",&lt;br&gt;
  "name": "MyTool",&lt;br&gt;
  "applicationCategory": "DeveloperApplication",&lt;br&gt;
  "operatingSystem": "Web",&lt;br&gt;
  "offers": {&lt;br&gt;
    "@type": "Offer",&lt;br&gt;
    "price": "0",&lt;br&gt;
    "priceCurrency": "USD"&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;h3&gt;
  
  
  FAQPage
&lt;/h3&gt;

&lt;p&gt;When users ask "Does X support Y?" in Perplexity, the answer often comes from FAQ schema. Each &lt;code&gt;mainEntity&lt;/code&gt; with &lt;code&gt;acceptedAnswer&lt;/code&gt; is a direct Q&amp;amp;A pair AIs extract verbatim.&lt;/p&gt;

&lt;h3&gt;
  
  
  Article
&lt;/h3&gt;

&lt;p&gt;For blog posts and docs, use &lt;code&gt;Article&lt;/code&gt; or &lt;code&gt;TechArticle&lt;/code&gt; with &lt;code&gt;datePublished&lt;/code&gt;, &lt;code&gt;author&lt;/code&gt;, and &lt;code&gt;headline&lt;/code&gt;. AI search engines prioritize schema dates over visible text when answering "is this current?"&lt;/p&gt;

&lt;h3&gt;
  
  
  WebApplication
&lt;/h3&gt;

&lt;p&gt;If you offer a browser-based tool, this tells AI crawlers it's an interactive app (not just a landing page). Include &lt;code&gt;browserRequirements&lt;/code&gt; and &lt;code&gt;softwareVersion&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes That Break AI Extraction
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Missing &lt;code&gt;@id&lt;/code&gt;&lt;/strong&gt;: Without a unique identifier, AI models can't distinguish your schema from similar ones on the page. Always add &lt;code&gt;@id: "https://yoursite.com/#app"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong &lt;code&gt;@context&lt;/code&gt; URL&lt;/strong&gt;: Some generators output &lt;code&gt;"@context": "http://schema.org"&lt;/code&gt; instead of &lt;code&gt;"https://schema.org"&lt;/code&gt;. The &lt;code&gt;https&lt;/code&gt; variant is the current standard — the &lt;code&gt;http&lt;/code&gt; one works but is deprecated and some AI parsers treat it as reduced confidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nested types without explicit roles&lt;/strong&gt;: If your software is also a product, use &lt;code&gt;additionalType&lt;/code&gt; rather than nesting blindly. Flat, explicit schemas parse more reliably than deeply nested ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Missing required properties&lt;/strong&gt;: Each schema type has required fields. SoftwareApplication demands &lt;code&gt;applicationCategory&lt;/code&gt; and &lt;code&gt;operatingSystem&lt;/code&gt;. Omitting them means the schema block may be ignored entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validating Your Schema
&lt;/h2&gt;

&lt;p&gt;Google's Rich Results Test is fine for traditional SEO, but it doesn't simulate AI extraction. For that, copy your JSON-LD into a validator that checks for: valid &lt;code&gt;@context&lt;/code&gt;, all required properties, correct pricing format, and resolvable URLs in &lt;code&gt;@id&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;I build lightweight browser-based tools at utilitylab.dev. If you want a free, no-login tool to generate and validate JSON-LD schema optimized for AI search engines like OpenAI, Perplexity, and Claude, there's one at &lt;a href="https://utilitylab.dev/ai-search-schema-generator" rel="noopener noreferrer"&gt;https://utilitylab.dev/ai-search-schema-generator&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Calculate DPI for Print Design (And Why 72 PPI Is the Wrong Default)</title>
      <dc:creator>Robert Wallace</dc:creator>
      <pubDate>Tue, 30 Jun 2026 03:26:54 +0000</pubDate>
      <link>https://dev.to/utilitylab/how-to-calculate-dpi-for-print-design-and-why-72-ppi-is-the-wrong-default-fhn</link>
      <guid>https://dev.to/utilitylab/how-to-calculate-dpi-for-print-design-and-why-72-ppi-is-the-wrong-default-fhn</guid>
      <description>&lt;p&gt;If you've ever designed something for print and had it come out blurry or the wrong size, the culprit is almost always one thing: &lt;strong&gt;wrong DPI assumptions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Most designers start with the screen default — 72 or 96 pixels per inch — and treat it like a universal constant. It's not. That number only describes your monitor. Print, photo exports, and physical mockups have completely different requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  The basic math
&lt;/h3&gt;

&lt;p&gt;DPI / PPI is just a ratio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DPI = pixel count / physical size in inches
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So if you have a 3000×4000 pixel image and you want to print it at 10×13.3 inches, the math is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Width: 3000 / 10 = &lt;strong&gt;300 DPI&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Height: 4000 / 13.3 = &lt;strong&gt;300 DPI&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the sweet spot for most offset printing. Below 250 DPI and you'll start seeing softness on coated stock. Above 350 DPI is usually unnecessary and just bloats file size.&lt;/p&gt;

&lt;h3&gt;
  
  
  When 300 DPI isn't the answer
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Billboards / large format&lt;/strong&gt;: viewed from far away, 30–75 DPI is fine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web / social&lt;/strong&gt;: 72–150 PPI is plenty because screens are fixed-pixel devices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retina / high-DPI screens&lt;/strong&gt;: CSS reference pixel is 96, but the device may have 2× or 3× physical pixel density&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mistake is treating "print DPI" and "screen PPI" as the same number. They aren't.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why designers get this wrong
&lt;/h3&gt;

&lt;p&gt;Three common traps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Resampling in Photoshop without checking actual dimensions&lt;/strong&gt; — changing the DPI metadata doesn't add real detail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exporting at 72 PPI from Figma/Sketch&lt;/strong&gt; — the artboard size in pixels is what matters, not the PPI label.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring bleed and trim&lt;/strong&gt; — a standard US letter page with 0.125" bleed needs the file to be oversized before imposition.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The faster way to check
&lt;/h3&gt;

&lt;p&gt;Instead of doing mental math, calculate the actual pixel density from the physical dimensions and native resolution. For a 27-inch 1440p monitor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2560 / (27" × √(16/9)) ≈ 109 PPI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That tells you whether text will look sharp at arm's length — it's not a print spec, it's a viewing-distance spec.&lt;/p&gt;

&lt;h3&gt;
  
  
  A practical check before you export
&lt;/h3&gt;

&lt;p&gt;Before sending a file to press or framing a photo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Note the source pixel dimensions&lt;/li&gt;
&lt;li&gt;Note the target physical size&lt;/li&gt;
&lt;li&gt;Divide&lt;/li&gt;
&lt;li&gt;Compare to the target medium's recommended density&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're designing for multiple outputs — web, Instagram, and a 16×20 print — each needs its own crop/resample pass. One file rarely fits all destinations.&lt;/p&gt;




&lt;p&gt;I build lightweight browser-based dev tools at &lt;a href="//utilitylab.dev"&gt;utilitylab.dev&lt;/a&gt;. If you want a fast client-side DPI/PPI check without installing anything, there's one there.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>design</category>
      <category>beginners</category>
      <category>tooling</category>
    </item>
    <item>
      <title>How to Clean CSV Files Before Importing into Excel</title>
      <dc:creator>Robert Wallace</dc:creator>
      <pubDate>Sun, 28 Jun 2026 09:41:33 +0000</pubDate>
      <link>https://dev.to/utilitylab/how-to-clean-csv-files-before-importing-into-excel-3l8c</link>
      <guid>https://dev.to/utilitylab/how-to-clean-csv-files-before-importing-into-excel-3l8c</guid>
      <description>&lt;p&gt;Every analyst and developer has been here: you open a CSV export and the headers are a mess — extra spaces, inconsistent casing, random line breaks, and half the columns have names that won’t survive a database import.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before you start cleaning in Excel, fix the source file. Excel will only make it worse.

Headers are the first thing I normalize. Trim whitespace, convert to lowercase or title case consistently, and replace spaces with underscores or hyphens. Consistent headers prevent JOIN failures, ambiguous column references, and broken analytics queries downstream.

Then remove empty rows and summarize rows. Exports from payment processors, ad platforms, and CRMs often sneak in totals or blank spacer rows. They look fine in a spreadsheet but break scripts and ETL pipelines the moment you automate the import.

Duplicate rows are another silent problem. They don’t always show up visually depending on the viewer, but they inflate metrics and corrupt aggregations. A quick deduplication pass on a stable key column — email, transaction ID, or timestamp + user ID — fixes this before it pollutes your dataset.

The last step is checking encoding. UTF-8 with BOM is the safest default. If your file opens with strange characters or column shifts, it’s almost always an encoding mismatch between the export tool and your target system.

If you’re doing this regularly, a browser-based cleaner that doesn’t upload the file is usually enough for one-off prep. It keeps the data local and avoids the friction of installing a preprocessing script for a single cleanup job.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>csv</category>
      <category>data</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Estimate GPT-4 API Costs Before Building</title>
      <dc:creator>Robert Wallace</dc:creator>
      <pubDate>Sun, 28 Jun 2026 09:38:30 +0000</pubDate>
      <link>https://dev.to/utilitylab/how-to-estimate-gpt-4-api-costs-before-building-4b3h</link>
      <guid>https://dev.to/utilitylab/how-to-estimate-gpt-4-api-costs-before-building-4b3h</guid>
      <description>&lt;p&gt;If you’re building an AI feature, the first thing to model is tokens — not model choice. Pricing pages show per-1K-token rates, but they don’t multiply the math for your actual prompt and completion patterns.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The mistake I see most often is estimating from a single “hello world” call. That number is almost always wrong because real usage has three cost layers: input tokens from system prompts, output tokens from model responses, and hidden costs from retries, logging, or repeated calls in loops.

Input cost is usually predictable if you count your system prompt and the average user message length. Output cost is where variance hides — it depends on max response length, temperature settings, and whether your app chains multiple model calls per interaction.

A practical approach before writing any production code: estimate prompt, completion, and training tokens separately. Run the pricing math at your expected daily call volume, then add a 2x buffer for traffic spikes and experimental prompts.

The trap nobody budgets for is long-running costs from prompt caching, context window growth, or feature expansion that quietly increases token counts. Modeling the ranges — minimum, expected, and worst case — before launch prevents the “why is my bill so high” moment later.

If you’re comparing providers or models, build the estimate table once and swap rates. The math is the same regardless of whether you’re using GPT-4, Claude, or another provider — only the per-1K-token numbers change.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to Fix Messy Markdown from Notion or Obsidian</title>
      <dc:creator>Robert Wallace</dc:creator>
      <pubDate>Sun, 28 Jun 2026 09:35:34 +0000</pubDate>
      <link>https://dev.to/utilitylab/how-to-fix-messy-markdown-from-notion-or-obsidian-3pp2</link>
      <guid>https://dev.to/utilitylab/how-to-fix-messy-markdown-from-notion-or-obsidian-3pp2</guid>
      <description>&lt;p&gt;If you’ve ever exported from Notion or Obsidian, you know the pain: extra whitespace everywhere, empty links, inconsistent heading styles, and stray HTML that breaks your site or docs build.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The good news is the content is usually fine. It just needs normalization.

Headings are the first thing I fix. Exports often start with H2 or mix H1 and H2 depending on how the document was structured. Pick a single top-level heading style and normalize everything down to it. Tools that rewrite headings automatically save more time than manually editing each file.

Then I remove empty bullets and blank lines. They look harmless in an editor, but they show up as layout problems in static site generators and markdown linters. Stripping them out before import prevents a lot of downstream noise.

Links are the next cleanup step. Exported docs love to include anchors to nowhere — placeholders, internal cross-references, or empty hrefs. Removing those late is worth it because broken links are harder to spot once the content is staged.

Finally, code fences should have language tags. Obsidian usually preserves them; Notion often drops them. Adding them back makes the content readable in GitHub, dev.to, or any docs renderer.

If you’re pasting a doc into a markdown cleaner, the fastest workflow is: normalize headings, remove empty bullets, strip dead links, then verify the result in a renderer before using it in production.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>productivity</category>
      <category>markdown</category>
      <category>webdev</category>
      <category>documentation</category>
    </item>
    <item>
      <title>Container Queries vs Media Queries: When to Use Which</title>
      <dc:creator>Robert Wallace</dc:creator>
      <pubDate>Sun, 28 Jun 2026 08:24:44 +0000</pubDate>
      <link>https://dev.to/utilitylab/container-queries-vs-media-queries-when-to-use-which-5m2</link>
      <guid>https://dev.to/utilitylab/container-queries-vs-media-queries-when-to-use-which-5m2</guid>
      <description>&lt;p&gt;I’ve been thinking about this lately: media queries handle page-level layout changes fine, but they fall apart when the same component needs to behave differently depending on where it sits in the DOM.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;That’s where container queries change the equation.

Media queries respond to the viewport. Container queries respond to the component’s container size. This matters for reusable design systems: a card component might sit in a 200px sidebar in one place and an 800px grid in another. With media queries alone, you’d need extra classes, wrapper changes, or JavaScript to handle that. Container queries let the component adapt based on its own context.

The practical rule I use: keep media queries for the page shell and major layout tiers. Use container queries inside reusable components that appear in multiple contexts. This keeps the CSS predictable without duplicating component variants.

A few real-world constraints worth knowing:
- Browser support is solid in modern browsers, but older versions still need fallbacks
- Container queries can’t replace media queries for user preferences like prefers-color-scheme or prefers-reduced-motion
- The syntax (cqw, cqh, container-type) takes a few minutes to internalize

If you’re experimenting with container query syntax, it helps to generate query blocks and paste them directly into your components instead of hand-writing each one. Testing a few variations is faster than guessing values in the editor.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>css</category>
      <category>frontend</category>
      <category>ui</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
