<?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: Ahnhyeongkyu</title>
    <description>The latest articles on DEV Community by Ahnhyeongkyu (@ahnhyeongkyu).</description>
    <link>https://dev.to/ahnhyeongkyu</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3786389%2F1d8d4f5f-6fd9-4e83-bd3f-8d55903a2420.png</url>
      <title>DEV Community: Ahnhyeongkyu</title>
      <link>https://dev.to/ahnhyeongkyu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ahnhyeongkyu"/>
    <language>en</language>
    <item>
      <title>We shipped 4 SaaS products and made almost nothing. Here's what we got wrong.</title>
      <dc:creator>Ahnhyeongkyu</dc:creator>
      <pubDate>Tue, 02 Jun 2026 15:21:32 +0000</pubDate>
      <link>https://dev.to/ahnhyeongkyu/we-shipped-4-saas-products-and-made-almost-nothing-heres-what-we-got-wrong-21ff</link>
      <guid>https://dev.to/ahnhyeongkyu/we-shipped-4-saas-products-and-made-almost-nothing-heres-what-we-got-wrong-21ff</guid>
      <description>&lt;p&gt;Three of our four products can take payments right now. Combined, they've made almost nothing. For a while I thought the hard part was building them. That was the easy part. Here's the honest version, and what we're changing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it's set up
&lt;/h2&gt;

&lt;p&gt;We run this as a small AI-agent operation. One terminal acts as the CEO: strategy, what to build, reviewing the work. Another is the CTO and writes the actual code. The products: a stats helper, a testimonial widget, an AI resume builder, a timeboxing app.&lt;/p&gt;

&lt;p&gt;It ships fast. That was never the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we shipped recently
&lt;/h2&gt;

&lt;p&gt;A normal day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;timebox: pulled out a placeholder "mock user" (everyone saw the same fake data) and wired in real Supabase login&lt;/li&gt;
&lt;li&gt;added Pro gating: free tier has limits, Pro unlocks the rest&lt;/li&gt;
&lt;li&gt;got cold email running from our own domain with a proper warmup&lt;/li&gt;
&lt;li&gt;added UTM tracking across three products so we can see where traffic actually goes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Six commits, all verified, all live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it actually broke
&lt;/h2&gt;

&lt;p&gt;Three products are live and take money. Total revenue is a rounding error.&lt;/p&gt;

&lt;p&gt;We kept building and never built a way for anyone to find it. No audience, no distribution. Ship something, post it once, move on to the next build. It felt productive and it was exactly the trap. You can't out-build a distribution problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we're changing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Stop starting new products. We have enough.&lt;/li&gt;
&lt;li&gt;Cold email to founders who'd actually use the widget. Real 1:1 reach, not blasting.&lt;/li&gt;
&lt;li&gt;Build-in-public (this post included) to grow an audience from zero.&lt;/li&gt;
&lt;li&gt;Measure first. Let the data say which product has real demand, then go all-in there instead of guessing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The numbers are small and we're early. That's the point of writing it down now, while it's still messy. More as it happens.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>saas</category>
      <category>indiehackers</category>
      <category>startup</category>
    </item>
    <item>
      <title>Build in public, month 2: 615 of 616 visitors never clicked anything</title>
      <dc:creator>Ahnhyeongkyu</dc:creator>
      <pubDate>Sat, 30 May 2026 19:32:34 +0000</pubDate>
      <link>https://dev.to/ahnhyeongkyu/build-in-public-month-2-615-of-616-visitors-never-clicked-anything-2341</link>
      <guid>https://dev.to/ahnhyeongkyu/build-in-public-month-2-615-of-616-visitors-never-clicked-anything-2341</guid>
      <description>&lt;p&gt;A follow-up in our series on running a SaaS portfolio with AI agents. Here are the real numbers from this cycle, no spin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Revenue (LemonSqueezy, statmate):&lt;/strong&gt; $26.36 lifetime, $6.59 in the last 30 days, $0.00 MRR, 0 active subscriptions. Every dollar so far is one-off credit — not a single recurring subscriber.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Funnel (IME-1, last 14 days):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;landing_view: 616&lt;/li&gt;
&lt;li&gt;cta_click: 1 (0.2%)&lt;/li&gt;
&lt;li&gt;signup_started: 0 (0.0%)&lt;/li&gt;
&lt;li&gt;paid_conversion: 0 (0.0%)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Overall landing→paid: 0.00%. The biggest drop is the very first step — 615 of 616 people left without clicking a single CTA.&lt;/p&gt;

&lt;p&gt;The lesson we're taking from this: it is not a checkout or pricing problem. You cannot A/B-test a stage that 99.8% of visitors never reach. When almost no one clicks, the bottleneck is upstream — traffic intent, not conversion rate. The one click we did get came from a "word-export-paywall" CTA: someone who wanted a specific utility, not the paid product.&lt;/p&gt;

&lt;p&gt;So the honest read on statmate.org: the organic search traffic we attract is task-intent and low purchase-intent. The lever isn't tuning the funnel — it's reaching people who actually intend to pay. More when the next data lands.&lt;/p&gt;




&lt;p&gt;The one product with real traffic right now: &lt;a href="https://statmate.org?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=buildinpublic-m2" rel="noopener noreferrer"&gt;statmate.org&lt;/a&gt; — a stats helper for students/researchers. I'm watching whether build-in-public readers actually click through and convert.&lt;/p&gt;

&lt;p&gt;Building something and need social proof that converts? That is what we are building at &lt;a href="https://trustfolio.dev?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=buildinpublic-m2" rel="noopener noreferrer"&gt;trustfolio.dev&lt;/a&gt; — and I am testing whether this build-in-public audience fits trustfolio better than the stats tool.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>startup</category>
      <category>buildinpublic</category>
      <category>saas</category>
    </item>
    <item>
      <title>An AI runs my company. A solo dev vibe-coded $15K in a week — we made $[X]. A cold autopsy.</title>
      <dc:creator>Ahnhyeongkyu</dc:creator>
      <pubDate>Sat, 30 May 2026 15:05:44 +0000</pubDate>
      <link>https://dev.to/ahnhyeongkyu/an-ai-runs-my-company-a-solo-dev-vibe-coded-15k-in-a-week-we-made-x-a-cold-autopsy-4fc2</link>
      <guid>https://dev.to/ahnhyeongkyu/an-ai-runs-my-company-a-solo-dev-vibe-coded-15k-in-a-week-we-made-x-a-cold-autopsy-4fc2</guid>
      <description>&lt;p&gt;An AI runs my company. Not as a demo — as the actual operator. Two Claude terminals (a "CEO" that does strategy and a "CTO" that writes the code), plus an agent orchestrator that's supposed to behave like a small staff. I am the chairman. I approve money and irreversible actions; the machine does the rest.&lt;/p&gt;

&lt;p&gt;Earlier this year a solo builder vibe-coded a product to roughly &lt;strong&gt;$15K in a week&lt;/strong&gt;. Same category of tools I have. Our number, after months and eight shipped products, is &lt;strong&gt;$[X]&lt;/strong&gt; — and $[X] is small enough that I'm using a placeholder out of dignity, not secrecy.&lt;/p&gt;

&lt;p&gt;This is the cold autopsy. No hype, no "AI changed everything." Just what actually happened and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers, unflattering
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;8 products shipped.&lt;/strong&gt; Live, deployable, real auth, real checkout wiring on several.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;~0 paying customers.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;One product (a small stats utility) has &lt;em&gt;real&lt;/em&gt; organic traffic — hundreds of search clicks and thousands of users a month. Paid conversion: effectively zero.&lt;/li&gt;
&lt;li&gt;The rest range from "launched to silence" to "domain now 404s."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you only looked at the build output, you'd think this was a competent shop. That was exactly the trap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Autopsy finding #1: I built a process-theater company
&lt;/h2&gt;

&lt;p&gt;I optimized the org for &lt;em&gt;activity&lt;/em&gt;, not &lt;em&gt;outcomes&lt;/em&gt;. The agents generated standups, status reports, "daily activity," kanban motion. It looked like a company. It produced almost no revenue.&lt;/p&gt;

&lt;p&gt;This isn't a unique personal failing — it's a documented one. There's now a published failure taxonomy for multi-agent LLM systems (search "MAST, Multi-Agent System failure taxonomy"). A large share of failures aren't bad models; they're &lt;strong&gt;specification and coordination failures&lt;/strong&gt;: agents that talk to each other, drift, re-litigate decisions, and confidently report progress on work that doesn't move a real metric. A "99-agent autonomous company" is not an achievement. It's the failure mode with a press release.&lt;/p&gt;

&lt;p&gt;The fix isn't more agents. It's fewer, gated, narrow workers whose output is checked by &lt;strong&gt;rules, not by another LLM's opinion&lt;/strong&gt;: did the URL return 200? did a payment land? did the PR merge? did the event fire in analytics? "An agent said it's done" is not evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Autopsy finding #2: build was never the bottleneck
&lt;/h2&gt;

&lt;p&gt;Both the $15K dev and I can build. Building is the commodity now. The difference was everything &lt;em&gt;around&lt;/em&gt; the build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Distribution &amp;amp; audience.&lt;/strong&gt; The $15K came from a person who already had eyes on them and shipped &lt;em&gt;in public&lt;/em&gt;. I shipped quietly into search engines and waited.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wave &amp;amp; willingness to pay.&lt;/strong&gt; They rode a hot category where early adopters spend money. I built a calculator for students in a saturated niche where the entire audience wants it free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus.&lt;/strong&gt; They pushed one thing end-to-end to revenue. I had eight half-finished things and an elaborate fake org chart.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three orders of magnitude of outcome, and &lt;em&gt;none&lt;/em&gt; of the gap was the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two technical war stories (because this is Dev.to)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The company brain died from a cron interval.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The agent backbone runs on serverless Postgres (Neon free tier). Free-tier compute &lt;strong&gt;auto-suspends after 5 minutes idle&lt;/strong&gt; — that's the whole cost model. At some point I "improved" responsiveness by bumping the agent heartbeat cron from every 30 minutes to &lt;strong&gt;every 5 minutes&lt;/strong&gt;. Feels harmless.&lt;/p&gt;

&lt;p&gt;It wasn't. A query every 5 minutes means the compute &lt;em&gt;never&lt;/em&gt; gets a 5-minute idle window. It stayed awake 24/7 — roughly 720 compute-hours/month against a free allowance closer to ~190. One day every query started returning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;HTTP 402: "exceeded the compute time quota"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six tables — agents, tasks, messages, approvals, events — all unreachable. The "company brain" was down, and it had been quietly burning toward this for weeks. Lesson: with scale-to-zero infra, &lt;strong&gt;your cron interval is a billing decision.&lt;/strong&gt; If it's at or under the suspend threshold, you've silently bought an always-on server.&lt;/p&gt;

&lt;p&gt;Worse: I had a daily quota monitor. It never fired — because it was pointed at the &lt;em&gt;wrong database&lt;/em&gt; (a legacy connection string), so it cheerfully reported "OK" while the real DB suffocated. Monitoring you don't verify is theater too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One webhook, four products, silent cross-contamination.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Several products check out through the same payment provider. That provider sends &lt;em&gt;all&lt;/em&gt; sales to a &lt;strong&gt;single account-level ping URL&lt;/strong&gt;. So a sale for product A was hitting product B's webhook and — because the handler trusted the payload — granting entitlements in the wrong app. The fix was a per-product discriminator (&lt;code&gt;url_params[workspace_id]&lt;/code&gt;) and a foreign-sale guard that drops anything that isn't ours. Multi-tenant billing is mostly about &lt;em&gt;refusing&lt;/em&gt; events, not processing them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the renewal actually is
&lt;/h2&gt;

&lt;p&gt;No grand relaunch. Concrete moves:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Kill process theater.&lt;/strong&gt; Retire the agent-as-staff fiction. Keep a few narrow workers with rule-based output gates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redefine "done."&lt;/strong&gt; "Done" = a test payment goes end-to-end (checkout → entitlement → feature works). Everything before that is a demo, and gets called a demo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distribution is now first-class&lt;/strong&gt;, co-equal with building — including writing this, in public, with the embarrassing number left in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure conversion, not motion.&lt;/strong&gt; Real funnel events or it didn't happen.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The honest close
&lt;/h2&gt;

&lt;p&gt;We made &lt;strong&gt;$[X]&lt;/strong&gt;. I'm not going to dress that up. But the diagnosis is unusually clear, and clear diagnoses are rare and cheap to act on: the machine can build all day; what it couldn't do is pick a market with a wave, earn an audience, and call a shell what it is.&lt;/p&gt;

&lt;p&gt;If you want to watch the renewal happen — including whether it works — the one product with real users is a small stats helper at &lt;strong&gt;&lt;a href="https://statmate.org?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=buildinpublic" rel="noopener noreferrer"&gt;statmate.org&lt;/a&gt;&lt;/strong&gt;, and the credibility-portfolio experiment is at &lt;strong&gt;&lt;a href="https://trustfolio.dev?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=buildinpublic" rel="noopener noreferrer"&gt;trustfolio.dev&lt;/a&gt;&lt;/strong&gt;. Both are deliberately public lab rats now.&lt;/p&gt;

&lt;p&gt;I'll post the next number whether it's up or down. That's the deal with building in public — the autopsy only counts if you keep the table open.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>startup</category>
      <category>buildinpublic</category>
      <category>programming</category>
    </item>
    <item>
      <title>We shipped a 5KB testimonial widget that uses AI to pick the most persuasive review</title>
      <dc:creator>Ahnhyeongkyu</dc:creator>
      <pubDate>Thu, 28 May 2026 14:53:44 +0000</pubDate>
      <link>https://dev.to/ahnhyeongkyu/we-shipped-a-5kb-testimonial-widget-that-uses-ai-to-pick-the-most-persuasive-review-55hb</link>
      <guid>https://dev.to/ahnhyeongkyu/we-shipped-a-5kb-testimonial-widget-that-uses-ai-to-pick-the-most-persuasive-review-55hb</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Testimonials are the highest-leverage copy on your landing page. Most SaaS founders ship the worst three they have.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem we ran into
&lt;/h2&gt;

&lt;p&gt;We were running a normal SaaS landing page. Twelve testimonials. Pretty headshots. Real customers.&lt;/p&gt;

&lt;p&gt;Conversion was flat.&lt;/p&gt;

&lt;p&gt;When we A/B tested swapping the testimonial block in and out, the lift was within margin of error. We started reading the actual reviews. Two of them were great. The other ten were variations of &lt;em&gt;"the product is good and the team is friendly."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Polite. True. Useless.&lt;/p&gt;

&lt;p&gt;We were doing what every SaaS founder does: dumping every review we could collect into a section and hoping volume = trust. It doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we shipped
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://trustfolio.dev" rel="noopener noreferrer"&gt;trustfolio&lt;/a&gt;&lt;/strong&gt; — a testimonial platform that collects reviews from your customers, scores each one on a 0-100 conviction scale using an LLM, and embeds the highest-converting ones on your site via a widget that ships in under 5KB.&lt;/p&gt;

&lt;p&gt;Three things make this different from the spreadsheet you're using now:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. A conviction score, not a star rating
&lt;/h3&gt;

&lt;p&gt;Five stars tells you a customer didn't hate you. It doesn't tell you whether the review will convert anyone.&lt;/p&gt;

&lt;p&gt;We score every incoming testimonial against four axes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Specificity&lt;/strong&gt; — does it name a measurable outcome ("cut onboarding from 3 weeks to 4 days") or a vague feeling ("really helpful")?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Objection-handling&lt;/strong&gt; — does it dismantle a doubt a prospect would have?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identifiability&lt;/strong&gt; — does the reader picture themselves in the customer's shoes?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tension and resolution&lt;/strong&gt; — does it tell a story, or is it a sentence?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 92-conviction testimonial belongs on your hero. A 41 belongs in the archive.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Smart highlights instead of full quotes
&lt;/h3&gt;

&lt;p&gt;A 180-word testimonial buries the line you want. We surface the 12 words that actually do the work, link them to the full quote on click, and serve the rest only if the reader keeps reading.&lt;/p&gt;

&lt;p&gt;This is one of the largest lifts we see in customer A/B tests: average widget click-throughs went up 2.3x when we replaced full quotes with smart highlights.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. A 5KB widget that respects your Lighthouse score
&lt;/h3&gt;

&lt;p&gt;Most testimonial widgets are 60-200KB of React + dependencies. They tank LCP and tag along third-party fetches.&lt;/p&gt;

&lt;p&gt;Ours ships as a single self-contained ESM bundle: 4.8KB gzipped, zero runtime deps, lazy-loads the rest only when the widget enters the viewport.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;async&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.trustfolio.dev/v1/widget.js"&lt;/span&gt;
        &lt;span class="na"&gt;data-project=&lt;/span&gt;&lt;span class="s"&gt;"your-slug"&lt;/span&gt;
        &lt;span class="na"&gt;data-layout=&lt;/span&gt;&lt;span class="s"&gt;"card-row"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole install. Six layouts available, all under the same size budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we learned building this
&lt;/h2&gt;

&lt;p&gt;A few things were not obvious until we shipped it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conviction scoring needs anchoring examples.&lt;/strong&gt; The first version of our scorer drifted: the same review would get a 71 one day and an 86 the next. We fixed it by anchoring each score to a fixed reference set of customer-graded examples. Drift dropped to single digits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Customers will give you better reviews if you ask better questions.&lt;/strong&gt; The default "leave a review" prompt produces "great product." A 3-question collection form (situation → friction → outcome) produces reviews that score 30+ points higher on conviction, on average.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Video is hard, but solvable.&lt;/strong&gt; We chose to do auto-transcription + AI-pulled timestamps for the moment in a video where the customer says the thing. Video testimonials with auto-pulled highlight clips outperformed text on our own funnel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free&lt;/strong&gt; — 15 testimonials, one widget layout, basic AI analysis. Good for landing pages with light traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pro — $29/mo&lt;/strong&gt; — unlimited text, 50 video/mo, all 6 layouts, full AI features.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business — $79/mo&lt;/strong&gt; — unlimited projects + video, A/B testing, API, white-label.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No credit card on free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you have a landing page and ten or more existing testimonials, drop them into trustfolio in 5 minutes and see what your highest-conviction quote actually is. Most founders are surprised.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://trustfolio.dev" rel="noopener noreferrer"&gt;trustfolio.dev&lt;/a&gt; — Show customer reviews, convert 34% more.&lt;/p&gt;

&lt;p&gt;If you ship this on a real page, I'd love to hear about your lift in the comments. We're collecting before/after numbers across the launch cohort.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>marketing</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Tried 10 ChatGPT Resume Prompts. Here's What Actually Got Me Interviews.</title>
      <dc:creator>Ahnhyeongkyu</dc:creator>
      <pubDate>Tue, 28 Apr 2026 03:49:21 +0000</pubDate>
      <link>https://dev.to/ahnhyeongkyu/i-tried-10-chatgpt-resume-prompts-heres-what-actually-got-me-interviews-kjo</link>
      <guid>https://dev.to/ahnhyeongkyu/i-tried-10-chatgpt-resume-prompts-heres-what-actually-got-me-interviews-kjo</guid>
      <description>&lt;p&gt;I sent 47 applications with my old resume. 0 callbacks.&lt;/p&gt;

&lt;p&gt;Then I tried 10 ChatGPT prompts I'd been collecting. I sent the next 12 applications with rewritten resumes. 4 callbacks. Here are the prompts that actually worked, and the ones that wasted my time.&lt;/p&gt;

&lt;p&gt;The pattern that worked: every prompt that produced something useful was specific about output format and gave the model permission to leave things blank. Every prompt that failed was vague.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt 1: STAR with [needs metric] markers
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"Rewrite my resume bullets using the STAR method (situation, task, action, result). Use the job description below as context. If a bullet doesn't have a measurable result, mark it [needs metric] instead of inventing one."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This was the unlock. The "[needs metric]" instruction stopped ChatGPT from hallucinating numbers. Half my old bullets came back marked, which forced me to actually go check Slack archives and find real outcomes. The other half came back tightened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt 2: Keyword gap analysis (don't add — identify)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"Read the job description. List the 10 most important keywords (skills, tools, methodologies) it uses. Then check my resume and tell me which keywords I'm missing or underusing."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Don't ask the AI to add keywords. Ask which ones are missing. Then you decide which ones genuinely apply.&lt;/p&gt;

&lt;p&gt;This is critical for ATS without resume-stuffing — the bot detects keyword density above ~2-3% and downgrades you for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt 3: 3-sentence summary cap
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"Rewrite my summary in 3 sentences max. Sentence 1: who I am professionally. Sentence 2: my biggest measurable achievement. Sentence 3: what I'm looking for next."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The 3-sentence constraint is what makes this work. Without the cap, ChatGPT writes corporate filler ("results-driven professional with a passion for...").&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt 4: ❌ "Make my resume more impressive"
&lt;/h2&gt;

&lt;p&gt;The one that failed for me. Vague prompts produce vague output. Skip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt 5: Multi-dimension scoring
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"Compare my resume to the job description. Score the match 0-100 across these 5 dimensions: keyword overlap, experience relevance, seniority match, industry fit, recency. Explain each score in one sentence."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Forces structured feedback you can actually act on. "Your industry fit is 30/100 because the JD mentions 'fintech' 4 times and your resume has none" is fixable. "Your resume could be better" is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt 6: Weakness audit
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"What's the weakest bullet on my resume? Why is it weak? Suggest a rewrite if I provide more context — but if you don't have enough context, ask me 1 specific question."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The "ask me a question" branch is what makes this useful. Most ChatGPT outputs are confidently wrong. This forces it to admit when it needs more info.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompts 7-10
&lt;/h2&gt;

&lt;p&gt;I'll spare you the full list. The structure is what matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Specific output format&lt;/strong&gt; (numbered, capped, marked)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission to leave things blank&lt;/strong&gt; ([needs metric], "I don't know")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap on length&lt;/strong&gt; (3 sentences, 5 bullets, 100 words)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare-don't-fix framing&lt;/strong&gt; (score it before fixing it)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  After spending a weekend running these manually...
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://getresumeai.site" rel="noopener noreferrer"&gt;ResumeAI&lt;/a&gt; to do all 10 automatically. Same prompts (refined), career-situation branching (switcher / returning / new grad / senior), and 23-criteria ATS scoring built in.&lt;/p&gt;

&lt;p&gt;It's $9 once. No subscription.&lt;/p&gt;

&lt;p&gt;If you want to do it manually, the 5 prompts above are enough. If you want it in 30 seconds, that's the link.&lt;/p&gt;

</description>
      <category>chatgpt</category>
      <category>resume</category>
      <category>ai</category>
      <category>career</category>
    </item>
    <item>
      <title>When Paddle Live Checkout Silently Broke - A Literal Backslash-N Debugging Story</title>
      <dc:creator>Ahnhyeongkyu</dc:creator>
      <pubDate>Sat, 18 Apr 2026 23:27:52 +0000</pubDate>
      <link>https://dev.to/ahnhyeongkyu/when-paddle-live-checkout-silently-broke-a-literal-backslash-n-debugging-story-2e5o</link>
      <guid>https://dev.to/ahnhyeongkyu/when-paddle-live-checkout-silently-broke-a-literal-backslash-n-debugging-story-2e5o</guid>
      <description>&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;Paddle's domain approval for trustfolio.dev came through on a Thursday afternoon. I flipped the environment from sandbox to production, clicked &lt;strong&gt;Upgrade&lt;/strong&gt; on my own dashboard, and got an overlay saying &lt;strong&gt;"Something went wrong. Please try again later."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No stack trace. No meaningful Paddle.js error object — just &lt;code&gt;{ error: {}, meta: {} }&lt;/code&gt; when I tried to capture it. Production logs looked fine. Webhook endpoint responded 200 OK for test pings. All the env vars were set. All the Paddle dashboard pieces existed.&lt;/p&gt;

&lt;p&gt;I spent an afternoon staring at this before figuring it out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hypothesis that was wrong
&lt;/h2&gt;

&lt;p&gt;My first guess: Paddle's live API needed something the sandbox didn't. Maybe a tax category, maybe a stricter domain verification. I re-read the Paddle dashboard twice. Everything looked correct.&lt;/p&gt;

&lt;p&gt;Then I tried &lt;code&gt;Paddle.PricePreview&lt;/code&gt; — the client-side preview API — with the production price ID I had pulled from my Vercel env. It threw the same blank error.&lt;/p&gt;

&lt;p&gt;So I copied the price ID directly out of the Paddle dashboard and used &lt;strong&gt;that&lt;/strong&gt; string instead of the env var. &lt;code&gt;PricePreview&lt;/code&gt; worked immediately. Subtotal $79, discount $0, total $79.&lt;/p&gt;

&lt;p&gt;The problem was in my environment variable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The culprit
&lt;/h2&gt;

&lt;p&gt;I pulled all seven PADDLE_* env vars down with &lt;code&gt;vercel env pull&lt;/code&gt; and inspected the raw bytes of each value's tail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PADDLE_BUSINESS_PRICE_ID  last6hex=7a 33 67 68 5c 6e
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last two bytes are &lt;code&gt;0x5c&lt;/code&gt; and &lt;code&gt;0x6e&lt;/code&gt; — a literal backslash followed by the letter &lt;code&gt;n&lt;/code&gt;. &lt;strong&gt;Not a real newline&lt;/strong&gt; (&lt;code&gt;0x0a&lt;/code&gt;). Just the two characters &lt;code&gt;\n&lt;/code&gt; stuck onto every value as a plain string.&lt;/p&gt;

&lt;p&gt;I had written a defensive &lt;code&gt;envTrim()&lt;/code&gt; earlier in the day that used JavaScript's &lt;code&gt;String.prototype.trim()&lt;/code&gt; to strip whitespace. But &lt;code&gt;trim()&lt;/code&gt; only removes actual whitespace — space, tab, newline, carriage return. It does &lt;strong&gt;not&lt;/strong&gt; remove the two-character string &lt;code&gt;\n&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So when my webhook compared &lt;code&gt;priceId === process.env.PADDLE_BUSINESS_PRICE_ID&lt;/code&gt;, it was really comparing &lt;code&gt;'pri_01knw040zen6akf7mrncx5z3gh'&lt;/code&gt; to &lt;code&gt;'pri_01knw040zen6akf7mrncx5z3gh\n'&lt;/code&gt;. Equality test failed silently. The webhook fell back to its default branch and labeled every incoming purchase as a Pro subscription, regardless of what was actually bought.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix in two layers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Layer 1 — code is defensive from now on.&lt;/strong&gt; I replaced every &lt;code&gt;.trim()&lt;/code&gt; in the Paddle code path with a regex that handles both whitespace and literal escape sequences:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;envTrim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(?:\\&lt;/span&gt;&lt;span class="sr"&gt;n|&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sr"&gt;r|&lt;/span&gt;&lt;span class="se"&gt;\s)&lt;/span&gt;&lt;span class="sr"&gt;+$/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;(?:\\&lt;/span&gt;&lt;span class="sr"&gt;n|&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sr"&gt;r|&lt;/span&gt;&lt;span class="se"&gt;\s)&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches the case even if somebody copy-pastes an env var with a trailing &lt;code&gt;\n&lt;/code&gt; again in the future.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2 — Vercel env vars are now clean.&lt;/strong&gt; A one-shot Node script pulled each variable, stripped the trailing junk, and re-registered it with &lt;code&gt;vercel env rm&lt;/code&gt; / &lt;code&gt;vercel env add&lt;/code&gt;. After a redeploy, the raw bytes looked exactly as expected. PricePreview and Checkout both succeeded immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the bad characters got there
&lt;/h2&gt;

&lt;p&gt;I never typed &lt;code&gt;\n&lt;/code&gt; into a Vercel env value on purpose. My best guess is that I pasted from a terminal or a note where a line ending got escaped as the literal two-character sequence, and Vercel stored it verbatim. Vercel's UI doesn't render trailing control characters, so the value looked clean in the dashboard.&lt;/p&gt;

&lt;p&gt;This is the kind of bug where the platform behaves correctly by any reasonable definition. The string you give it is the string it stores. The surprise is entirely in the mismatch between what you think the string is and what it actually is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons worth carrying forward
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;When an API returns a blank error object, suspect your own input before suspecting the service.&lt;/strong&gt; Especially for services like Paddle that have been running at scale for years. The bug is usually on your side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hex-inspect env vars when checkout mysteriously breaks.&lt;/strong&gt; &lt;code&gt;vercel env pull&lt;/code&gt; plus a five-line Node script saved me hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Defensive trimming should handle literal escape sequences, not just whitespace.&lt;/strong&gt; Especially for values that cross system boundaries — env vars, webhooks, CSV imports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep a webhook-inspection utility ready.&lt;/strong&gt; My DB monitor that printed every &lt;code&gt;workspaces&lt;/code&gt; row change let me see exactly where the plan comparison was going wrong.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why this matters for SaaS reliability
&lt;/h2&gt;

&lt;p&gt;Checkout reliability is the whole game for a paid SaaS. A silent 5% failure rate on upgrade attempts is more expensive than any growth channel. If you run a small SaaS, take an afternoon and try to break your checkout in weird ways — paste env vars from a shell history, use a discount code for an unrelated product, hit the checkout from a Safari incognito window. You will find at least one bug. The question is whether you find it or a paying customer does.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Trustfolio&lt;/strong&gt; collects customer testimonials, scores them with AI for conversion potential, and embeds them anywhere with a single &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag. Free tier, no credit card.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://trustfolio.dev" rel="noopener noreferrer"&gt;trustfolio.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>saas</category>
      <category>javascript</category>
      <category>paddle</category>
    </item>
    <item>
      <title>How Trustfolio's testimonial widget stays under 5KB — while the rest of the market ships 65–125KB</title>
      <dc:creator>Ahnhyeongkyu</dc:creator>
      <pubDate>Sat, 18 Apr 2026 07:59:18 +0000</pubDate>
      <link>https://dev.to/ahnhyeongkyu/how-trustfolios-testimonial-widget-stays-under-5kb-while-the-rest-of-the-market-ships-65-125kb-32ee</link>
      <guid>https://dev.to/ahnhyeongkyu/how-trustfolios-testimonial-widget-stays-under-5kb-while-the-rest-of-the-market-ships-65-125kb-32ee</guid>
      <description>&lt;p&gt;I've been running Trustfolio for a few months now, and the one decision that keeps coming up in sales calls is the widget bundle size.&lt;/p&gt;

&lt;p&gt;People don't buy a testimonial tool because it's 4.8KB. But they stop looking at alternatives the moment they realize the competitor's widget drops their Lighthouse score by 15 points.&lt;/p&gt;

&lt;p&gt;So here are the exact choices that kept our embed under 5KB while the category average sits at &lt;strong&gt;65–125KB gzip&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark (measured with Chrome DevTools, 2026)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Widget&lt;/th&gt;
&lt;th&gt;Layout&lt;/th&gt;
&lt;th&gt;Transfer size (gzip)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Testimonial.to&lt;/td&gt;
&lt;td&gt;Wall&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;125 KB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Senja&lt;/td&gt;
&lt;td&gt;Carousel&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;65 KB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Famewall&lt;/td&gt;
&lt;td&gt;Wall&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;88 KB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trustfolio&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Wall&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.8 KB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trustfolio&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Carousel&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.9 KB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trustfolio&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Badge&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.1 KB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These numbers came from Chrome DevTools' Network tab with the "Disable cache" + "Slow 3G" options. I'm comparing equivalent rendered outputs, not the vendor's marketing numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choice #1 — Ship vanilla JS. No framework runtime.
&lt;/h2&gt;

&lt;p&gt;Every competitor I audited ships some flavor of React or a micro-framework at runtime. React plus ReactDOM alone is ~45KB gzip — that's already 9x our entire widget.&lt;/p&gt;

&lt;p&gt;Our embed is a single IIFE. It reads the &lt;code&gt;data-trustfolio&lt;/code&gt; attribute, fetches JSON from an edge route, and renders DOM directly. No JSX, no virtual DOM, no reconciler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mount&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-trustfolio]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trustfolio&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://trustfolio.dev/api/widgets/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/data`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shadow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachShadow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;closed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shadow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;render()&lt;/code&gt; is a small set of layout functions (wall, carousel, badge, popup, marquee, card) that directly assemble DOM nodes. Each layout lazy-loads its handlers on first interaction — carousel's swipe handler is only fetched when the user actually swipes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choice #2 — Shadow DOM, not CSS-in-JS.
&lt;/h2&gt;

&lt;p&gt;Testimonial widgets have to deal with hostile host styles. Your widget might be rendered inside a WordPress theme that has &lt;code&gt;* { box-sizing: border-box }&lt;/code&gt; on every element, or a Tailwind Preflight that strips all default margins.&lt;/p&gt;

&lt;p&gt;Most vendors solve this by shipping their styles inline or via CSS-in-JS — which is why you see &lt;code&gt;styled-components&lt;/code&gt; or &lt;code&gt;emotion&lt;/code&gt; in their bundles.&lt;/p&gt;

&lt;p&gt;We use a &lt;strong&gt;closed Shadow DOM&lt;/strong&gt;. The shadow root has its own style scope. No host style can leak in. No widget style can leak out. And we don't ship a runtime because the browser handles encapsulation natively.&lt;/p&gt;

&lt;p&gt;Stylesheet is a &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; element inside the shadow, declarative, zero runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choice #3 — &lt;code&gt;async&lt;/code&gt; script + &lt;code&gt;navigator.sendBeacon&lt;/code&gt;.
&lt;/h2&gt;

&lt;p&gt;The script is &lt;code&gt;async&lt;/code&gt;, so it never blocks rendering. Impression and click analytics use &lt;code&gt;navigator.sendBeacon&lt;/code&gt;, which queues the request in the browser's network stack and returns instantly — no awaiting a fetch promise, no perf regression on user interaction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;})],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendBeacon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/analytics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The analytics endpoint writes to an edge-adjacent table. No third-party tracking scripts. No cookies. The widget is GDPR-trivial because it collects nothing a user wouldn't expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choice #4 — No web fonts.
&lt;/h2&gt;

&lt;p&gt;We render testimonials with &lt;code&gt;font-family: system-ui, -apple-system, sans-serif&lt;/code&gt;. Native UI font on every platform. Zero KB of font download. Zero FOIT/FOUT.&lt;/p&gt;

&lt;p&gt;Competitors that load Google Fonts pay 20–80KB per weight for the privilege of a slightly prettier wall of quotes. Not worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choice #5 — Bundle once, not per layout.
&lt;/h2&gt;

&lt;p&gt;All six layouts share the same bundle. Dead-code elimination via esbuild removes unused layouts at build time — but the runtime itself is one file. The tradeoff: the carousel user downloads ~0.7KB of wall-specific code they'll never run. The win: one HTTP request instead of six, one cached URL instead of a cache-busting nightmare per layout combination.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this unlocks for customers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Shopify product pages that still score &amp;gt;95 on Lighthouse with social proof embedded.&lt;/li&gt;
&lt;li&gt;Framer landing pages that keep their signature "single HTTP request" speed.&lt;/li&gt;
&lt;li&gt;WordPress sites that don't need to install a plugin (just a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is magical. It's just a refusal to ship more than the feature requires. Every line of code I considered adding, I asked: "does this change what the user actually sees?" If not, it didn't go in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wider point
&lt;/h2&gt;

&lt;p&gt;A lot of modern SaaS ships weight by default because frameworks make it easy and bundlers don't warn you until it's already too late.&lt;/p&gt;

&lt;p&gt;Testimonial widgets are a small niche, but the same pattern applies to every embedded SaaS asset — chat bubbles, cookie banners, analytics scripts, A/B testing agents. They accumulate until your site is 60% third-party JS.&lt;/p&gt;

&lt;p&gt;If you're building one, the question worth asking is: &lt;strong&gt;what's the smallest thing that still delivers the feature?&lt;/strong&gt; Not "what's the easiest stack to reach for?"&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Trustfolio&lt;/strong&gt; collects customer testimonials, scores them with AI for conversion potential, and embeds them on any site with a single &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag. Free tier, 15 testimonials, no credit card.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://trustfolio.dev" rel="noopener noreferrer"&gt;trustfolio.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PS: for the first founder reading this, &lt;code&gt;FOUNDER100&lt;/code&gt; gives you 100% off the first month of Pro or Business. 24 hours, one use.&lt;/p&gt;

</description>
      <category>webperf</category>
      <category>javascript</category>
      <category>saas</category>
      <category>performance</category>
    </item>
    <item>
      <title>I Built a Testimonial.to Alternative — Here's What I Learned</title>
      <dc:creator>Ahnhyeongkyu</dc:creator>
      <pubDate>Mon, 13 Apr 2026 18:02:53 +0000</pubDate>
      <link>https://dev.to/ahnhyeongkyu/i-built-a-testimonialto-alternative-hwebdev-javascript-saas-buildinpubliceres-what-i-learned-2le3</link>
      <guid>https://dev.to/ahnhyeongkyu/i-built-a-testimonialto-alternative-hwebdev-javascript-saas-buildinpubliceres-what-i-learned-2le3</guid>
      <description>&lt;p&gt;I've been shipping products for a while now — StatMate, RoastSite, CodeNeat, RestInLight, and a few others. Every single one of them had the same problem: I'd get great feedback from users in DMs and emails, but none of it was visible on my landing pages.&lt;/p&gt;

&lt;p&gt;I looked at Testimonial.to. Great product, $2.4M ARR, clearly solving a real problem. But three things bugged me:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The widget was heavy.&lt;/strong&gt; Loading a 50KB+ widget to show three quotes felt wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS conflicts were constant.&lt;/strong&gt; Their widget styles would clash with my Tailwind setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No intelligence layer.&lt;/strong&gt; All testimonials were treated equally.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So I built &lt;a href="https://trustfolio.dev" rel="noopener noreferrer"&gt;Trustfolio&lt;/a&gt;. Here's the technical story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shadow DOM for Style Isolation
&lt;/h2&gt;

&lt;p&gt;If you've ever embedded a third-party widget on your site, you know the CSS collision nightmare. Shadow DOM solves this completely. The widget renders inside an encapsulated DOM tree with its own scoped styles. Zero leakage in either direction.&lt;/p&gt;

&lt;p&gt;The result: embed one line of code, and the widget looks exactly the same on every site, regardless of what CSS framework the host page uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sub-5KB Challenge
&lt;/h2&gt;

&lt;p&gt;Competitors ship 50-125KB widget bundles. Ours is under 5KB. How?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No framework runtime (vanilla JS, no React/Vue/Svelte)&lt;/li&gt;
&lt;li&gt;No CSS-in-JS library&lt;/li&gt;
&lt;li&gt;No external font loading&lt;/li&gt;
&lt;li&gt;Aggressive tree-shaking and minification&lt;/li&gt;
&lt;li&gt;Single IIFE bundle, zero external dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  AI Conversion Scoring
&lt;/h2&gt;

&lt;p&gt;This is the experimental part. Every testimonial gets analyzed on ingestion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sentiment detection&lt;/strong&gt; — genuine enthusiasm vs. polite filler&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specificity scoring&lt;/strong&gt; — "saved 10 hours/week" scores higher than "great tool"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conversion prediction&lt;/strong&gt; — 0-100 score based on specificity + emotion + outcome mentions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where I Am Now
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Product is live at &lt;a href="https://trustfolio.dev" rel="noopener noreferrer"&gt;trustfolio.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Free tier: 15 testimonials, full AI features&lt;/li&gt;
&lt;li&gt;Pro: $29/mo, Business: $79/mo&lt;/li&gt;
&lt;li&gt;Using it on all my own products (dogfooding)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What I'd love feedback on:&lt;/strong&gt; Is AI conversion scoring genuinely useful, or is it solving a problem that doesn't exist?&lt;/p&gt;

&lt;p&gt;Happy to answer any technical questions in the comments.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>saas</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Built 8 Developer Tools That Never See Your Code</title>
      <dc:creator>Ahnhyeongkyu</dc:creator>
      <pubDate>Sun, 12 Apr 2026 22:59:28 +0000</pubDate>
      <link>https://dev.to/ahnhyeongkyu/i-built-8-developer-tools-that-never-see-youwebdev-javascript-privacy-toolsr-code-4ma7</link>
      <guid>https://dev.to/ahnhyeongkyu/i-built-8-developer-tools-that-never-see-youwebdev-javascript-privacy-toolsr-code-4ma7</guid>
      <description>&lt;h2&gt;
  
  
  The Moment I Realized the Problem
&lt;/h2&gt;

&lt;p&gt;A few months ago, I was debugging a production issue at midnight. I had a large JSON config file that was not parsing correctly. I did what every developer does — I opened the first Google result for "json formatter online" and pasted my config.&lt;/p&gt;

&lt;p&gt;Then it hit me. That config contained database connection strings, API keys, and internal service URLs. I opened DevTools and checked the Network tab.&lt;/p&gt;

&lt;p&gt;Sure enough: a POST request carrying my entire input to the site's server. My production credentials had just traveled across the internet to a server I knew nothing about.&lt;/p&gt;

&lt;p&gt;I checked three more popular formatting sites. Same pattern. Every one of them sent the input to their backend for processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  This Is Not a Theoretical Risk
&lt;/h2&gt;

&lt;p&gt;Think about what developers paste into online tools on a daily basis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JSON files&lt;/strong&gt; containing API keys, database credentials, and service configurations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Base64 strings&lt;/strong&gt; that encode authentication tokens and binary data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL queries&lt;/strong&gt; revealing table names, column structures, and business logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWTs&lt;/strong&gt; containing user identities, roles, and session data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regex patterns&lt;/strong&gt; tested against real production data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code diffs&lt;/strong&gt; showing proprietary source code changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The leading online developer tool sites handle tens of millions of visits per month. That is an enormous volume of sensitive developer data flowing through third-party servers every day.&lt;/p&gt;

&lt;p&gt;And here is the thing: &lt;strong&gt;none of these operations require a server.&lt;/strong&gt; JSON formatting, Base64 encoding, URL encoding, regex matching, text diffing, JWT decoding, SQL formatting, hash generation — all of these can be done entirely in the browser with JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I Built CodeNeat
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://codeneat.dev" rel="noopener noreferrer"&gt;CodeNeat&lt;/a&gt; is a set of 8 developer tools where every operation runs 100% in your browser. No data is ever sent to any server for processing.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 8 Tools
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;a href="https://codeneat.dev/json-formatter" rel="noopener noreferrer"&gt;JSON Formatter &amp;amp; Viewer&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
Format, validate, minify, and explore JSON with syntax highlighting and interactive tree view.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;a href="https://codeneat.dev/base64-encode-decode" rel="noopener noreferrer"&gt;Base64 Encode/Decode&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
Encode and decode Base64 strings. Instant results with zero server round-trip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;a href="https://codeneat.dev/url-encode-decode" rel="noopener noreferrer"&gt;URL Encode/Decode&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
Full Unicode support for URL encoding and decoding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. &lt;a href="https://codeneat.dev/regex-tester" rel="noopener noreferrer"&gt;Regex Tester&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
Real-time matching with capture groups and flag controls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. &lt;a href="https://codeneat.dev/diff-checker" rel="noopener noreferrer"&gt;Diff Checker&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
Side-by-side text comparison with line-level highlighting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. &lt;a href="https://codeneat.dev/jwt-decoder" rel="noopener noreferrer"&gt;JWT Decoder&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
Decode and inspect JWTs — header, payload, signature, expiration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. &lt;a href="https://codeneat.dev/sql-formatter" rel="noopener noreferrer"&gt;SQL Formatter&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
Multi-dialect SQL formatting. Your queries never leave the browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. &lt;a href="https://codeneat.dev/hash-generator" rel="noopener noreferrer"&gt;Hash Generator&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
MD5, SHA-1, SHA-256, SHA-512. All computation via Web Crypto API.&lt;/p&gt;
&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;Each tool is a pure TypeScript function in &lt;code&gt;lib/tools/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/tools/json.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;indent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;indent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These functions are imported directly into React client components. No &lt;code&gt;fetch()&lt;/code&gt;. No &lt;code&gt;XMLHttpRequest&lt;/code&gt;. No server endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Verify
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Method 1: DevTools
&lt;/h3&gt;

&lt;p&gt;Open any tool on &lt;a href="https://codeneat.dev" rel="noopener noreferrer"&gt;codeneat.dev&lt;/a&gt;, open DevTools Network tab, filter by Fetch/XHR, use the tool. Zero processing requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 2: Offline
&lt;/h3&gt;

&lt;p&gt;Load any tool, disconnect from the internet, use the tool. It works because the processing never needed a server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 3: Source Code
&lt;/h3&gt;

&lt;p&gt;Open source: &lt;a href="https://github.com/Ahnhyeongkyu/codeneat" rel="noopener noreferrer"&gt;github.com/Ahnhyeongkyu/codeneat&lt;/a&gt;. Search for &lt;code&gt;fetch(&lt;/code&gt; in &lt;code&gt;lib/tools/&lt;/code&gt;. Zero results.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Framework:&lt;/strong&gt; Next.js 16 with App Router&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language:&lt;/strong&gt; TypeScript&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Styling:&lt;/strong&gt; Tailwind CSS v4 + shadcn/ui&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment:&lt;/strong&gt; Vercel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Is Next
&lt;/h2&gt;

&lt;p&gt;A Chrome extension that brings these tools into the browser toolbar. The core logic ports directly since it is pure functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://codeneat.dev" rel="noopener noreferrer"&gt;codeneat.dev&lt;/a&gt; — free, no account required.&lt;/p&gt;

&lt;p&gt;If useful: &lt;a href="https://github.com/Ahnhyeongkyu/codeneat" rel="noopener noreferrer"&gt;Star on GitHub&lt;/a&gt; to help others discover it.&lt;/p&gt;

&lt;p&gt;Your code should stay yours. That is not a feature — it should be the default.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>security</category>
      <category>showdev</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Stop Shipping 300KB Testimonial Widgets — There's a Better Way</title>
      <dc:creator>Ahnhyeongkyu</dc:creator>
      <pubDate>Sat, 28 Mar 2026 04:29:09 +0000</pubDate>
      <link>https://dev.to/ahnhyeongkyu/stop-shipping-300kb-testimonial-wiwebdev-saas-javascript-testimonialsdgets-theres-a-better-way-3039</link>
      <guid>https://dev.to/ahnhyeongkyu/stop-shipping-300kb-testimonial-wiwebdev-saas-javascript-testimonialsdgets-theres-a-better-way-3039</guid>
      <description>&lt;p&gt;If you've ever added a testimonial widget to your site, you probably noticed the performance hit. Most popular solutions load 200-300KB of JavaScript, inject iframes, and tank your Core Web Vitals.&lt;/p&gt;

&lt;p&gt;I run five different web products. Every one of them needed social proof. And every testimonial tool I tried made the same trade-off: nice-looking widgets that quietly destroyed page speed.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://trustfolio.dev" rel="noopener noreferrer"&gt;Trustfolio&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Here's what a typical testimonial widget does to your site:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;200-300KB&lt;/strong&gt; of JavaScript loaded on every page&lt;/li&gt;
&lt;li&gt;Layout shift from async-loaded content&lt;/li&gt;
&lt;li&gt;Extra DNS lookups and third-party connections&lt;/li&gt;
&lt;li&gt;No way to know which testimonials actually drive conversions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Trustfolio Is Different
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Under 5KB Widget
&lt;/h3&gt;

&lt;p&gt;The entire embed script is under 5KB gzipped. It uses Shadow DOM for style isolation, loads async, and renders in under 100ms. Zero layout shift.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-trustfolio=&lt;/span&gt;&lt;span class="s"&gt;"YOUR_WIDGET_ID"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://trustfolio.dev/embed.js"&lt;/span&gt; &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Two lines. No iframes, no external CSS, no layout shift.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. AI That Knows What Converts
&lt;/h3&gt;

&lt;p&gt;Instead of manually picking which testimonials to show, Trustfolio's AI analyzes every submission:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sentiment Analysis&lt;/strong&gt; — scores emotional tone from 0 to 1&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart Highlights&lt;/strong&gt; — finds the most persuasive phrases automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conversion Score&lt;/strong&gt; — rates each testimonial 0-100 based on how likely it is to drive action&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-Tagging&lt;/strong&gt; — categorizes by topic (support, pricing, features, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Six Widget Layouts
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layout&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wall of Love&lt;/td&gt;
&lt;td&gt;Landing pages, dedicated testimonial sections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Carousel&lt;/td&gt;
&lt;td&gt;Limited space, above the fold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Badge&lt;/td&gt;
&lt;td&gt;Compact trust signals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single Card&lt;/td&gt;
&lt;td&gt;Hero sections, sidebars&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Popup&lt;/td&gt;
&lt;td&gt;Exit intent, time-delayed social proof&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marquee&lt;/td&gt;
&lt;td&gt;Continuous scrolling banners&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  4. Multiple Collection Methods
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom collection pages&lt;/strong&gt; — branded forms with your questions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import from CSV&lt;/strong&gt; — migrate from other tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google/G2 imports&lt;/strong&gt; — pull existing reviews (more sources coming)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Technical Architecture
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16&lt;/strong&gt; with Edge Runtime for the OG image API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neon (serverless Postgres)&lt;/strong&gt; via Drizzle ORM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shadow DOM&lt;/strong&gt; (closed mode) for widget style isolation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sendBeacon&lt;/strong&gt; for analytics tracking without blocking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI analysis&lt;/strong&gt; powered by Claude API&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free&lt;/strong&gt;: 10 testimonials, 1 widget, wall layout&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pro&lt;/strong&gt; ($29/mo): Unlimited testimonials, all layouts, AI analysis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business&lt;/strong&gt; ($79/mo): Everything + API access, white label, A/B testing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;If you're running a SaaS, portfolio, or any site that shows customer feedback, give it a shot: &lt;a href="https://trustfolio.dev" rel="noopener noreferrer"&gt;trustfolio.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Free tier is genuinely usable — no credit card, no trial expiration. I'd appreciate any feedback from the dev community.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I Built a Sub-5KB Testimonial Widget Because Every Alternative Tanked My Lighthouse Score</title>
      <dc:creator>Ahnhyeongkyu</dc:creator>
      <pubDate>Fri, 27 Mar 2026 11:51:25 +0000</pubDate>
      <link>https://dev.to/ahnhyeongkyu/i-built-a-sub-5kb-testimonial-widget-becauswebdev-javascript-saas-aie-every-alternative-tanked-55l4</link>
      <guid>https://dev.to/ahnhyeongkyu/i-built-a-sub-5kb-testimonial-widget-becauswebdev-javascript-saas-aie-every-alternative-tanked-55l4</guid>
      <description>&lt;p&gt;Every testimonial widget I tried added 200-500KB to my pages. My Lighthouse scores dropped. My ads got more expensive because pages loaded slower.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://trustfolio.dev" rel="noopener noreferrer"&gt;Trustfolio&lt;/a&gt; — a testimonial platform where the entire embed widget is under 5KB gzipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Most testimonial tools treat performance as an afterthought. They load heavy JavaScript bundles, inject their own CSS that conflicts with yours, and add hundreds of milliseconds to your page load.&lt;/p&gt;

&lt;p&gt;Research shows each second of delay reduces conversions by 4.42%. If you're running paid ads, slow pages = wasted ad spend.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Trustfolio&lt;/strong&gt; is a full testimonial management platform:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Collect&lt;/strong&gt;: Shareable collection pages (text + video, no sign-up needed for customers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analyze&lt;/strong&gt;: AI-powered sentiment analysis, theme detection, and highlight suggestions using Claude Haiku&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Display&lt;/strong&gt;: 5 widget layouts — Wall, Carousel, Badge, Popup, Marquee&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track&lt;/strong&gt;: Conversion tracking to see which testimonials actually drive sign-ups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The embed widget uses Shadow DOM for zero CSS conflicts and loads in under 10ms from CDN.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Next.js + React&lt;/li&gt;
&lt;li&gt;Drizzle ORM + Neon PostgreSQL (serverless)&lt;/li&gt;
&lt;li&gt;Clerk Auth (GitHub + Google + Email)&lt;/li&gt;
&lt;li&gt;Claude Haiku API for AI analysis&lt;/li&gt;
&lt;li&gt;Shadow DOM embed widget (3.1KB gzipped)&lt;/li&gt;
&lt;li&gt;Vercel deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;Trustfolio is live with a generous free tier — 15 testimonials, 1 widget, full AI analysis. No credit card required.&lt;/p&gt;

&lt;p&gt;Check it out: &lt;a href="https://trustfolio.dev" rel="noopener noreferrer"&gt;trustfolio.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd love to hear your feedback!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Regex Lookahead and Lookbehind: Patterns Every Developer Should Know</title>
      <dc:creator>Ahnhyeongkyu</dc:creator>
      <pubDate>Wed, 04 Mar 2026 02:40:11 +0000</pubDate>
      <link>https://dev.to/ahnhyeongkyu/regex-lookahead-and-lookbehind-patterns-every-developer-should-know-2lb</link>
      <guid>https://dev.to/ahnhyeongkyu/regex-lookahead-and-lookbehind-patterns-every-developer-should-know-2lb</guid>
      <description>&lt;p&gt;Lookaheads and lookbehinds are the regex features that separate "I know regex" from "I &lt;em&gt;know&lt;/em&gt; regex." They let you match patterns based on what comes before or after — without including that context in the match itself.&lt;/p&gt;

&lt;p&gt;Once you understand them, problems that seemed impossible with basic regex become straightforward. Let's break them down.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Lookaheads and Lookbehinds?
&lt;/h2&gt;

&lt;p&gt;Think of regular expressions as a cursor moving through text. Normally, every part of your pattern consumes characters — the cursor advances as it matches. Lookaheads and lookbehinds are different: they &lt;strong&gt;peek&lt;/strong&gt; at surrounding text without consuming it. The cursor looks, but doesn't move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An analogy:&lt;/strong&gt; Imagine you're searching a bookshelf for books with red covers. A normal regex is like pulling each book off the shelf to check it. A lookahead is like glancing at the next book without pulling it out. A lookbehind is like glancing at the previous book. You're gathering information, but you're not removing anything from the shelf.&lt;/p&gt;

&lt;p&gt;This "zero-width" property is what makes them powerful. You can assert that certain text exists nearby without including it in your match result.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Four Types: A Syntax Reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Syntax&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Positive Lookahead&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(?=...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What follows &lt;strong&gt;must&lt;/strong&gt; match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Negative Lookahead&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(?!...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What follows &lt;strong&gt;must not&lt;/strong&gt; match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Positive Lookbehind&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(?&amp;lt;=...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What precedes &lt;strong&gt;must&lt;/strong&gt; match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Negative Lookbehind&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(?&amp;lt;!...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What precedes &lt;strong&gt;must not&lt;/strong&gt; match&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key to remembering the syntax:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;=&lt;/code&gt; means positive (it &lt;strong&gt;does&lt;/strong&gt; match)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;!&lt;/code&gt; means negative (it &lt;strong&gt;doesn't&lt;/strong&gt; match)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;&lt;/code&gt; means look &lt;strong&gt;behind&lt;/strong&gt; (to the left)&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;&amp;lt;&lt;/code&gt; means look &lt;strong&gt;ahead&lt;/strong&gt; (to the right)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now let's put them to work with five real-world examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 1: Password Validation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; Validate that a password contains at least one uppercase letter, one lowercase letter, one digit, and is at least 8 characters long.&lt;/p&gt;

&lt;p&gt;Without lookaheads, you'd need to check each condition separately or write an absurdly complex alternation. With lookaheads, you can stack multiple conditions at the same position:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;passwordRegex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;(?=&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;A-Z&lt;/span&gt;&lt;span class="se"&gt;])(?=&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;a-z&lt;/span&gt;&lt;span class="se"&gt;])(?=&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;\d)&lt;/span&gt;&lt;span class="sr"&gt;.&lt;/span&gt;&lt;span class="se"&gt;{8,}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Test cases&lt;/span&gt;
&lt;span class="nx"&gt;passwordRegex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MyPass1234&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// true — has upper, lower, digit, 8+ chars&lt;/span&gt;
&lt;span class="nx"&gt;passwordRegex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mypass1234&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// false — no uppercase&lt;/span&gt;
&lt;span class="nx"&gt;passwordRegex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MYPASS1234&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// false — no lowercase&lt;/span&gt;
&lt;span class="nx"&gt;passwordRegex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MyPassword&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// false — no digit&lt;/span&gt;
&lt;span class="nx"&gt;passwordRegex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Mp1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;         &lt;span class="c1"&gt;// false — too short&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How it works, step by step:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;^&lt;/code&gt; — Start of string&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(?=.*[A-Z])&lt;/code&gt; — Lookahead: somewhere ahead there's an uppercase letter. The cursor doesn't move.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(?=.*[a-z])&lt;/code&gt; — Lookahead: somewhere ahead there's a lowercase letter. Still at position 0.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(?=.*\d)&lt;/code&gt; — Lookahead: somewhere ahead there's a digit. Still at position 0.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.{8,}$&lt;/code&gt; — Now actually consume: match 8 or more of any character to the end.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All three lookaheads fire from the same starting position. They each independently scan the entire string for their condition. Only when all three succeed does the engine proceed to the actual match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding more rules&lt;/strong&gt; is trivial. Need a special character too?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;strongPassword&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;(?=&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;A-Z&lt;/span&gt;&lt;span class="se"&gt;])(?=&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;a-z&lt;/span&gt;&lt;span class="se"&gt;])(?=&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;\d)(?=&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;!@#$%^&amp;amp;*&lt;/span&gt;&lt;span class="se"&gt;])&lt;/span&gt;&lt;span class="sr"&gt;.&lt;/span&gt;&lt;span class="se"&gt;{10,}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example 2: Extract Prices Without Currency Symbols
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; You have text containing prices in various formats and you want to extract just the numeric values, without the currency symbol.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Products: $49.99, €120.00, £75.50, $1,299.00&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Positive lookbehind: match numbers preceded by a currency symbol&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;priceRegex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(?&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;=&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;$€£&lt;/span&gt;&lt;span class="se"&gt;])\d[\d&lt;/span&gt;&lt;span class="sr"&gt;,.&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;priceRegex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// =&amp;gt; ['49.99', '120.00', '75.50', '1,299.00']&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;(?&amp;lt;=[$€£])&lt;/code&gt; — Lookbehind: the position must be preceded by &lt;code&gt;$&lt;/code&gt;, &lt;code&gt;€&lt;/code&gt;, or &lt;code&gt;£&lt;/code&gt;. This symbol is NOT included in the match.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;\d[\d,.]*\d&lt;/code&gt; — Match digits, possibly with commas and decimal points in between.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The beauty here is that the currency symbols are used to locate the right numbers but aren't captured. Without a lookbehind, you'd need a capture group and an extra extraction step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Without lookbehind (less clean)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;$€£&lt;/span&gt;&lt;span class="se"&gt;](\d[\d&lt;/span&gt;&lt;span class="sr"&gt;,.&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\d)&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lookbehind version is more direct and readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 3: Match Words NOT Followed by Specific Text
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; In a codebase, find all uses of &lt;code&gt;import&lt;/code&gt; that are NOT followed by &lt;code&gt;type&lt;/code&gt; (you want value imports, not TypeScript type imports).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;`
import React from 'react';
import type { FC } from 'react';
import { useState } from 'react';
import type { Props } from './types';
import axios from 'axios';
&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;;

const valueImports = /import(?!\s+type)\s+.+/g;
const matches = code.match(valueImports);
// =&amp;gt; [
//   "import React from 'react';",
//   "import { useState } from 'react';",
//   "import axios from 'axios';"
// ]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;import&lt;/code&gt; — Match the literal text "import"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(?!\s+type)&lt;/code&gt; — Negative lookahead: what follows must NOT be whitespace + "type"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;\s+.+&lt;/code&gt; — Then consume the rest of the import statement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The negative lookahead acts as a filter. It says "yes, I found &lt;code&gt;import&lt;/code&gt;, but only keep it if &lt;code&gt;type&lt;/code&gt; doesn't come next."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Another practical use — excluding test files:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Match .js files that are NOT test files&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;regex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\w&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(?!\.&lt;/span&gt;&lt;span class="sr"&gt;test&lt;/span&gt;&lt;span class="se"&gt;)\.&lt;/span&gt;&lt;span class="sr"&gt;js/g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app.js utils.test.js config.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// =&amp;gt; ['app.js', 'config.js']&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example 4: Parse Structured Log Files
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; You're parsing log entries and need to extract the log level, but only from lines that contain an IP address (indicating network-related events).&lt;/p&gt;

&lt;p&gt;Sample log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-03-01 10:15:32 [ERROR] Connection timeout from 192.168.1.100
2026-03-01 10:15:33 [INFO] Cache cleared successfully
2026-03-01 10:15:34 [WARN] Rate limit approaching for 10.0.0.55
2026-03-01 10:15:35 [DEBUG] Query executed in 42ms
2026-03-01 10:15:36 [ERROR] SSL handshake failed from 172.16.0.200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;logs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;`2026-03-01 10:15:32 [ERROR] Connection timeout from 192.168.1.100
2026-03-01 10:15:33 [INFO] Cache cleared successfully
2026-03-01 10:15:34 [WARN] Rate limit approaching for 10.0.0.55
2026-03-01 10:15:35 [DEBUG] Query executed in 42ms
2026-03-01 10:15:36 [ERROR] SSL handshake failed from 172.16.0.200&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;;

// Positive lookahead: match log level only if an IP address appears later in the line
const networkLogs = /\[(ERROR|WARN|INFO|DEBUG)\](?=.*\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/gm;

const matches = [...logs.matchAll(networkLogs)].map(m =&amp;gt; m[1]);
// =&amp;gt; ['ERROR', 'WARN', 'ERROR']
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;\[(ERROR|WARN|INFO|DEBUG)\]&lt;/code&gt; — Match the log level in brackets, capturing the level name&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(?=.*\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})&lt;/code&gt; — Lookahead: somewhere on this line, there's an IP address pattern&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lines without IP addresses (the INFO cache line and the DEBUG query line) are excluded. The lookahead lets you filter on content that appears much later in the line without having to match everything in between.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Combining with lookbehind to extract the IP itself:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Extract IPs that appear after the word "from"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fromIPs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(?&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;=from&lt;/span&gt;&lt;span class="se"&gt;\s)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fromIPs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// =&amp;gt; ['192.168.1.100', '172.16.0.200']&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example 5: Email Username Extraction
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; Extract the username portion of email addresses (everything before the &lt;code&gt;@&lt;/code&gt;) from a block of text.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;`
Contact us:
  - Support: support@codeneat.dev
  - Sales: sales.team@codeneat.dev
  - Bug reports: bugs+tracker@codeneat.dev
&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;;

// Positive lookahead: match word characters (plus dots, hyphens, and +)
// that are followed by @
const usernameRegex = /[\w.+-]+(?=@)/g;

const usernames = text.match(usernameRegex);
// =&amp;gt; ['support', 'sales.team', 'bugs+tracker']
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[\w.+-]+&lt;/code&gt; — Match one or more word characters, dots, plus signs, or hyphens&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(?=@)&lt;/code&gt; — Lookahead: this match must be followed by &lt;code&gt;@&lt;/code&gt;, but don't include &lt;code&gt;@&lt;/code&gt; in the result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without the lookahead, you'd match the &lt;code&gt;@&lt;/code&gt; and then have to strip it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Without lookahead&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;([\w&lt;/span&gt;&lt;span class="sr"&gt;.+-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;@/g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lookahead version is cleaner because the match itself is exactly what you want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Extracting the domain instead (using lookbehind):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;domainRegex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(?&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;=@&lt;/span&gt;&lt;span class="se"&gt;)[\w&lt;/span&gt;&lt;span class="sr"&gt;.-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domainRegex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// =&amp;gt; ['codeneat.dev', 'codeneat.dev', 'codeneat.dev']&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Browser and Engine Support
&lt;/h2&gt;

&lt;p&gt;Lookaheads have been supported everywhere for decades. Lookbehinds are newer — here's the current status:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Chrome&lt;/th&gt;
&lt;th&gt;Firefox&lt;/th&gt;
&lt;th&gt;Safari&lt;/th&gt;
&lt;th&gt;Node.js&lt;/th&gt;
&lt;th&gt;Edge&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Positive Lookahead &lt;code&gt;(?=...)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;All versions&lt;/td&gt;
&lt;td&gt;All versions&lt;/td&gt;
&lt;td&gt;All versions&lt;/td&gt;
&lt;td&gt;All versions&lt;/td&gt;
&lt;td&gt;All versions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Negative Lookahead &lt;code&gt;(?!...)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;All versions&lt;/td&gt;
&lt;td&gt;All versions&lt;/td&gt;
&lt;td&gt;All versions&lt;/td&gt;
&lt;td&gt;All versions&lt;/td&gt;
&lt;td&gt;All versions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Positive Lookbehind &lt;code&gt;(?&amp;lt;=...)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;62+&lt;/td&gt;
&lt;td&gt;78+&lt;/td&gt;
&lt;td&gt;16.4+&lt;/td&gt;
&lt;td&gt;8.10+&lt;/td&gt;
&lt;td&gt;79+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Negative Lookbehind &lt;code&gt;(?&amp;lt;!...)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;62+&lt;/td&gt;
&lt;td&gt;78+&lt;/td&gt;
&lt;td&gt;16.4+&lt;/td&gt;
&lt;td&gt;8.10+&lt;/td&gt;
&lt;td&gt;79+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; As of 2026, lookbehinds are safe to use in all modern browsers. The only concern is if you need to support Safari 16.3 or older (released early 2023). For server-side JavaScript (Node.js), lookbehinds have been available since Node 8.&lt;/p&gt;

&lt;p&gt;If you need to support older environments, you can always rewrite lookbehinds using capture groups:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Lookbehind version (modern)&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;(?&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;g
&lt;/span&gt;
&lt;span class="c1"&gt;// Capture group equivalent (universal)&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;  &lt;span class="c1"&gt;// then use match[1]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common Pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Variable-length lookbehinds:&lt;/strong&gt; JavaScript supports variable-length lookbehinds, but some regex engines (notably older Python &lt;code&gt;re&lt;/code&gt; module) don't. If your pattern works in JS but fails elsewhere, this might be why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Performance with &lt;code&gt;.*&lt;/code&gt; in lookaheads:&lt;/strong&gt; Patterns like &lt;code&gt;(?=.*something)&lt;/code&gt; cause the engine to scan the entire remaining string. In tight loops or very long strings, stack multiple specific lookaheads rather than using greedy quantifiers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Forgetting that lookarounds are zero-width:&lt;/strong&gt; A common mistake is expecting &lt;code&gt;(?=foo)bar&lt;/code&gt; to match "foobar". It won't — after the lookahead asserts "foo" is ahead, the cursor is still at the same position, and it tries to match "bar" starting where "foo" starts. You'd want &lt;code&gt;(?=foo)foobar&lt;/code&gt; or just &lt;code&gt;foobar&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Your Patterns Live
&lt;/h2&gt;

&lt;p&gt;Regex is a skill you build by doing. Reading about lookaheads is useful; writing and testing them is where the understanding clicks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test these patterns live at &lt;a href="https://codeneat.dev/regex-tester" rel="noopener noreferrer"&gt;codeneat.dev/regex-tester&lt;/a&gt;&lt;/strong&gt; — it provides real-time match highlighting, capture group visualization, and runs entirely in your browser so your test data stays private.&lt;/p&gt;

&lt;p&gt;Paste any of the examples from this article and experiment. Change the patterns, try different inputs, and build intuition for how lookaheads and lookbehinds actually work.&lt;/p&gt;

</description>
      <category>regex</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
