<?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: DevToolsmith</title>
    <description>The latest articles on DEV Community by DevToolsmith (@toolkitonline).</description>
    <link>https://dev.to/toolkitonline</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%2F3835996%2F61f81be0-056b-4410-8e4d-d3c8f59aa05b.png</url>
      <title>DEV Community: DevToolsmith</title>
      <link>https://dev.to/toolkitonline</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/toolkitonline"/>
    <language>en</language>
    <item>
      <title>EU AI Act Compliance for SMEs: What You Actually Need to Do Before August 2026</title>
      <dc:creator>DevToolsmith</dc:creator>
      <pubDate>Thu, 09 Apr 2026 09:00:07 +0000</pubDate>
      <link>https://dev.to/toolkitonline/eu-ai-act-compliance-for-smes-what-you-actually-need-to-do-before-august-2026-3aea</link>
      <guid>https://dev.to/toolkitonline/eu-ai-act-compliance-for-smes-what-you-actually-need-to-do-before-august-2026-3aea</guid>
      <description>&lt;p&gt;The EU AI Act deadline for high-risk AI systems is &lt;strong&gt;August 2, 2026&lt;/strong&gt;. That's roughly 4 months away.&lt;/p&gt;

&lt;p&gt;If you're running an SME that uses AI — chatbots, recommendation engines, hiring tools, content moderation, credit scoring — this regulation applies to you. Not just to big tech. Not just to EU-based companies. Anyone serving EU customers.&lt;/p&gt;

&lt;p&gt;This guide cuts through the legal jargon and tells you exactly what to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Is Actually Affected?
&lt;/h2&gt;

&lt;p&gt;The AI Act uses a &lt;strong&gt;risk-based classification&lt;/strong&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  Unacceptable Risk (Banned)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Social scoring by governments&lt;/li&gt;
&lt;li&gt;Real-time biometric surveillance in public spaces&lt;/li&gt;
&lt;li&gt;Manipulation of vulnerable groups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;If you're doing any of these, stop. No compliance checklist will help.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  High Risk (Strict Requirements — August 2026 deadline)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HR/Recruitment&lt;/strong&gt;: AI screening resumes, ranking candidates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credit scoring&lt;/strong&gt;: AI assessing creditworthiness&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Education&lt;/strong&gt;: AI grading students, determining access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Healthcare&lt;/strong&gt;: AI assisting diagnosis or treatment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Law enforcement&lt;/strong&gt;: AI in predictive policing, evidence evaluation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Critical infrastructure&lt;/strong&gt;: AI managing energy, water, transport&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Limited Risk (Transparency obligations)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chatbots&lt;/strong&gt;: Must disclose they're AI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deepfakes&lt;/strong&gt;: Must be labeled as AI-generated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emotion recognition&lt;/strong&gt;: Must inform the user&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Minimal Risk (No obligations)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Spam filters, AI in video games, inventory management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Most SMEs fall into Limited or High Risk.&lt;/strong&gt; If you have a customer-facing chatbot, you're at minimum in the "limited risk" category with transparency obligations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5 Key Obligations for SMEs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Risk Classification
&lt;/h3&gt;

&lt;p&gt;First, classify every AI system you operate or deploy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What does it do?&lt;/strong&gt; (chatbot, recommendation, scoring, generation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who does it affect?&lt;/strong&gt; (employees, customers, general public)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's the impact?&lt;/strong&gt; (convenience vs. life-changing decisions)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Document this. Regulators will ask.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Technical Documentation
&lt;/h3&gt;

&lt;p&gt;For high-risk systems, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Description of the system's purpose and intended use&lt;/li&gt;
&lt;li&gt;Design specifications and development methodology&lt;/li&gt;
&lt;li&gt;Training data: sources, preparation, known biases&lt;/li&gt;
&lt;li&gt;Performance metrics and testing results&lt;/li&gt;
&lt;li&gt;Risk assessment and mitigation measures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For limited-risk systems, lighter documentation suffices — but "we use ChatGPT" is not documentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Human Oversight
&lt;/h3&gt;

&lt;p&gt;High-risk AI must have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A human who can understand the system's outputs&lt;/li&gt;
&lt;li&gt;The ability to override AI decisions&lt;/li&gt;
&lt;li&gt;Clear escalation procedures&lt;/li&gt;
&lt;li&gt;Logging of AI decisions for review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This doesn't mean a human reviews every decision. It means a human &lt;em&gt;can&lt;/em&gt; intervene when needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Transparency
&lt;/h3&gt;

&lt;p&gt;All AI systems interacting with people must:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clearly disclose they're AI (chatbots, virtual assistants)&lt;/li&gt;
&lt;li&gt;Label AI-generated content (images, text, audio)&lt;/li&gt;
&lt;li&gt;Inform users about automated decision-making&lt;/li&gt;
&lt;li&gt;Provide explanations when AI decisions affect individuals&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Data Governance
&lt;/h3&gt;

&lt;p&gt;Training data must be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Relevant and representative&lt;/li&gt;
&lt;li&gt;Free from known biases (or biases documented and mitigated)&lt;/li&gt;
&lt;li&gt;Compliant with GDPR (you're already doing this, right?)&lt;/li&gt;
&lt;li&gt;Properly versioned and traceable&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Self-Assess: Step by Step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Inventory (Week 1)
&lt;/h3&gt;

&lt;p&gt;List every AI system in your company. Include third-party tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customer support chatbot (e.g., Intercom with AI)&lt;/li&gt;
&lt;li&gt;Email marketing personalization&lt;/li&gt;
&lt;li&gt;HR screening tools&lt;/li&gt;
&lt;li&gt;Recommendation engines&lt;/li&gt;
&lt;li&gt;Content generation tools&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Classify (Week 2)
&lt;/h3&gt;

&lt;p&gt;For each system, determine the risk level using the categories above. When in doubt, classify higher.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Gap Analysis (Week 3)
&lt;/h3&gt;

&lt;p&gt;Compare current state with requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you have documentation? ❌/✅&lt;/li&gt;
&lt;li&gt;Is there human oversight? ❌/✅&lt;/li&gt;
&lt;li&gt;Are users informed? ❌/✅&lt;/li&gt;
&lt;li&gt;Is training data documented? ❌/✅&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 4: Remediate (Weeks 4-12)
&lt;/h3&gt;

&lt;p&gt;Prioritize by risk level. High-risk systems first. Start with documentation — it's the most time-consuming.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Ongoing Monitoring
&lt;/h3&gt;

&lt;p&gt;Compliance isn't a one-time event. You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Regular re-assessment (quarterly)&lt;/li&gt;
&lt;li&gt;Incident reporting procedures&lt;/li&gt;
&lt;li&gt;Updated documentation when systems change&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tools and Resources
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Automated assessment&lt;/strong&gt;: &lt;a href="https://complipilot.dev" rel="noopener noreferrer"&gt;CompliPilot&lt;/a&gt; runs 200+ automated checks against EU AI Act requirements. Free tier: 3 assessments/month. It classifies your AI systems, identifies gaps, and generates documentation templates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Official guidance&lt;/strong&gt;: The EU AI Office publishes &lt;a href="https://digital-strategy.ec.europa.eu/en/policies/regulatory-framework-ai" rel="noopener noreferrer"&gt;implementation guidelines&lt;/a&gt; — dense but authoritative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Legal counsel&lt;/strong&gt;: For high-risk systems, get a lawyer. Automated tools handle the checklist; lawyers handle the gray areas.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fines Are Real
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prohibited AI practices&lt;/strong&gt;: Up to EUR 35M or 7% of global annual turnover&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High-risk non-compliance&lt;/strong&gt;: Up to EUR 15M or 3% of turnover&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incorrect information to authorities&lt;/strong&gt;: Up to EUR 7.5M or 1% of turnover&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For SMEs, there are reduced fines — but "reduced" still means potentially business-ending amounts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Other EU Regulation You're Probably Ignoring
&lt;/h2&gt;

&lt;p&gt;While you're sorting AI compliance, check your website accessibility. The &lt;strong&gt;European Accessibility Act (EAA)&lt;/strong&gt; has been enforced since June 2025, with fines up to EUR 300K per violation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fixmyweb.dev" rel="noopener noreferrer"&gt;FixMyWeb&lt;/a&gt; scans your website for 201 WCAG accessibility issues in 60 seconds. Because getting fined for two EU regulations simultaneously would be embarrassing.&lt;/p&gt;

&lt;p&gt;And if your SaaS handles recurring payments, &lt;a href="https://paymentrescue.dev" rel="noopener noreferrer"&gt;PaymentRescue&lt;/a&gt; recovers 30-50% of failed payments automatically — because compliance costs money, and you'll want to plug revenue leaks elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Action Plan: April to August 2026
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Month&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;April&lt;/td&gt;
&lt;td&gt;Complete AI inventory and risk classification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May&lt;/td&gt;
&lt;td&gt;Begin documentation for high-risk systems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;June&lt;/td&gt;
&lt;td&gt;Implement transparency measures (chatbot disclosures, content labeling)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;July&lt;/td&gt;
&lt;td&gt;Complete human oversight procedures, test incident reporting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;August&lt;/td&gt;
&lt;td&gt;Final review, submit conformity assessment if required&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Start Now
&lt;/h2&gt;

&lt;p&gt;Four months sounds like a lot until you realize that documentation alone takes 4-6 weeks for a high-risk system.&lt;/p&gt;

&lt;p&gt;Run a &lt;a href="https://complipilot.dev" rel="noopener noreferrer"&gt;free compliance check&lt;/a&gt; today. You'll know in 5 minutes whether you have a problem — and exactly what to fix.&lt;/p&gt;

&lt;p&gt;The companies that start now will be compliant by August. The ones that wait until July will be scrambling. Don't be the latter.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Is your company preparing for the EU AI Act? What's been the biggest challenge? Share in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>compliance</category>
      <category>europe</category>
      <category>startup</category>
    </item>
    <item>
      <title>How to Build a Visual Monitoring System with a Screenshot API (Node.js + CaptureAPI)</title>
      <dc:creator>DevToolsmith</dc:creator>
      <pubDate>Sun, 05 Apr 2026 19:46:55 +0000</pubDate>
      <link>https://dev.to/toolkitonline/how-to-build-a-visual-monitoring-system-with-a-screenshot-api-nodejs-captureapi-4647</link>
      <guid>https://dev.to/toolkitonline/how-to-build-a-visual-monitoring-system-with-a-screenshot-api-nodejs-captureapi-4647</guid>
      <description>&lt;p&gt;Your website looked fine yesterday. Today, the hero banner is broken, the checkout button disappeared behind a modal, and nobody noticed until a customer complained on Twitter.&lt;/p&gt;

&lt;p&gt;Visual monitoring catches these issues &lt;em&gt;before&lt;/em&gt; your users do. In this tutorial, I'll walk you through building a complete visual monitoring system using Node.js and a screenshot API. By the end, you'll have a working tool that automatically captures screenshots of your web pages, compares them over time, and alerts you when something looks wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is Visual Monitoring?
&lt;/h2&gt;

&lt;p&gt;Visual monitoring is the practice of periodically capturing screenshots of your web pages and comparing them against a known-good baseline. Unlike functional testing (which checks if buttons work) or uptime monitoring (which checks if the server responds), visual monitoring catches &lt;em&gt;how things look&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This matters because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CSS changes can break layouts silently&lt;/li&gt;
&lt;li&gt;Third-party scripts (ads, chat widgets) can shift content&lt;/li&gt;
&lt;li&gt;CMS updates can overwrite templates&lt;/li&gt;
&lt;li&gt;Deployments can introduce visual regressions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;Here's what we're building:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+----------------+     +----------------+     +----------------+
|  Scheduler     |----&amp;gt;|  Screenshot    |----&amp;gt;|  Comparison    |
|  (cron job)    |     |  Capture       |     |  Engine        |
+----------------+     +----------------+     +----------------+
                                                      |
                                               +------v------+
                                               |  Alert      |
                                               |  System     |
                                               +-------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Components:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A scheduler that runs at defined intervals&lt;/li&gt;
&lt;li&gt;A screenshot capture service (using CaptureAPI)&lt;/li&gt;
&lt;li&gt;A pixel comparison engine&lt;/li&gt;
&lt;li&gt;An alerting system (email/Slack/webhook)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 18+&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://captureapi.dev" rel="noopener noreferrer"&gt;CaptureAPI&lt;/a&gt; account (free tier gives you 200 screenshots/month)&lt;/li&gt;
&lt;li&gt;Basic familiarity with async/await&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Project Setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;visual-monitor &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;visual-monitor
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;node-cron pixelmatch pngjs node-fetch@3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the folder structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; screenshots/&lt;span class="o"&gt;{&lt;/span&gt;baseline,current&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Capture Screenshots with CaptureAPI
&lt;/h2&gt;

&lt;p&gt;CaptureAPI provides a simple REST endpoint that returns a screenshot as a PNG. Here's the capture module:&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;// capture.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs/promises&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&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;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CAPTUREAPI_KEY&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;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.captureapi.dev/v1/screenshot&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;captureScreenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&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;params&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;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;viewport_width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1280&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;viewport_height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;800&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;full_page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fullPage&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;false&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// wait for dynamic content&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;response&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;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&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="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Screenshot failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusText&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&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;filepath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;screenshots&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;current&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Captured: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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;filepath&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;&lt;strong&gt;Key decisions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;delay&lt;/code&gt; parameter gives JavaScript-heavy pages time to render&lt;/li&gt;
&lt;li&gt;We default to 1280x800 viewport, which represents a common desktop resolution&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;full_page&lt;/code&gt; is off by default to keep comparison fast, but you can enable it for content-heavy pages&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Build the Pixel Comparison Engine
&lt;/h2&gt;

&lt;p&gt;This is where the magic happens. We'll use &lt;code&gt;pixelmatch&lt;/code&gt;, a fast pixel-level image comparison library:&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;// compare.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs/promises&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PNG&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pngjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;pixelmatch&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pixelmatch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;compareScreenshots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baselineFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentFile&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;baselineBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;screenshots&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;baseline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baselineFile&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;currentBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;screenshots&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;current&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentFile&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;baseline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PNG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baselineBuffer&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;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PNG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentBuffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Handle size mismatches (layout shift detection)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;diffPercentage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Size changed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;x&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;x&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;baseline&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;diff&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;PNG&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&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;mismatchedPixels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pixelmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;baseline&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="nx"&gt;current&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="nx"&gt;diff&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="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// tolerance for anti-aliasing&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;totalPixels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;height&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;diffPercentage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mismatchedPixels&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;totalPixels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Save diff image for debugging&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;diffPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;screenshots&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`diff-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;currentFile&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diffPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PNG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;diffPercentage&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// less than 0.5% difference = OK&lt;/span&gt;
    &lt;span class="na"&gt;diffPercentage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diffPercentage&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;mismatchedPixels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;diffImagePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;diffPath&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;&lt;strong&gt;Why 0.5% threshold?&lt;/strong&gt; In practice, sub-pixel rendering differences between captures can cause tiny variations. A 0.5% threshold filters out noise while catching real issues. You can tune this per page -- a mostly-static landing page might use 0.1%, while a page with animated elements might need 2%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Configure Monitoring Targets
&lt;/h2&gt;

&lt;p&gt;Define the pages you want to monitor in a simple config file:&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;// config.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;homepage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://yoursite.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pricing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://yoursite.com/pricing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// stricter -- pricing page should rarely change&lt;/span&gt;
    &lt;span class="na"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout-mobile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://yoursite.com/checkout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;375&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;812&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// iPhone viewport&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alertConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;webhookUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SLACK_WEBHOOK_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;emailTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ALERT_EMAIL&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Build the Alert System
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// alert.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;alertConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./config.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendAlert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&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;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Visual change detected on *&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pageName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;*`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;blocks&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="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;section&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;text&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;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s2"&gt;`*Visual Change Detected*`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;`Page: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pageName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;`Difference: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;diffPercentage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;`Pixels changed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mismatchedPixels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLocaleString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="o"&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;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&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="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// Slack webhook&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;alertConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhookUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&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="nx"&gt;alertConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhookUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&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="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&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="na"&gt;body&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;message&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="c1"&gt;// Console fallback&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`ALERT: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pageName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; changed by &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;diffPercentage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%`&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;h2&gt;
  
  
  Step 6: Wire Everything Together
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// monitor.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;cron&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node-cron&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;captureScreenshot&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./capture.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;compareScreenshots&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./compare.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sendAlert&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./alert.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./config.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runCheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&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;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.png`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;captureScreenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;height&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;result&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;compareScreenshots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;diffPercentage&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendAlert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;changed&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="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&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="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Error monitoring &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runAllChecks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`--- Visual check started at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&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;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;pages&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;page&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;runCheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&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;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&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;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="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fulfilled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;summary&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;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Diff %&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;diffPercentage&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="c1"&gt;// Run every 30 minutes&lt;/span&gt;
&lt;span class="nx"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*/30 * * * *&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;runAllChecks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Also run immediately on start&lt;/span&gt;
&lt;span class="nf"&gt;runAllChecks&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 7: Baseline Management
&lt;/h2&gt;

&lt;p&gt;You need a way to set and update baselines. Add this utility:&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;// baseline.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs/promises&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;captureScreenshot&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./capture.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./config.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateBaselines&lt;/span&gt;&lt;span class="p"&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;page&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;pages&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;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.png`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;captureScreenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Copy current to baseline&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;screenshots&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;current&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filename&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;dest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;screenshots&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;baseline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copyFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Baseline updated: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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;updateBaselines&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;node baseline.js&lt;/code&gt; after every intentional visual change to update your reference images.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running It in Production
&lt;/h2&gt;

&lt;p&gt;For production, add a &lt;code&gt;package.json&lt;/code&gt; scripts section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"monitor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node monitor.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"baseline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node baseline.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then deploy it however you prefer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker + cron&lt;/strong&gt;: Package it in a container and let the built-in &lt;code&gt;node-cron&lt;/code&gt; handle scheduling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions&lt;/strong&gt;: Use a scheduled workflow to run the check every hour&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Lambda + EventBridge&lt;/strong&gt;: Trigger the check function on a schedule&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A GitHub Actions example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/visual-monitor.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Visual Monitor&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*/2&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;  &lt;span class="c1"&gt;# every 2 hours&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# manual trigger&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node baseline.js&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;CAPTUREAPI_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CAPTUREAPI_KEY }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node monitor.js&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;CAPTUREAPI_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CAPTUREAPI_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;SLACK_WEBHOOK_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SLACK_WEBHOOK_URL }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Practical Tips From Real Usage
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Ignore dynamic regions.&lt;/strong&gt; Cookie banners, timestamps, and ads will trigger false positives. CaptureAPI supports &lt;code&gt;hide_selectors&lt;/code&gt; to hide elements before capture:&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;params&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;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;hide_selectors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.cookie-banner, .timestamp, .ad-slot&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;&lt;strong&gt;2. Monitor mobile &lt;em&gt;and&lt;/em&gt; desktop.&lt;/strong&gt; Many visual bugs only appear at specific viewports. Test at least 1280px (desktop), 768px (tablet), and 375px (mobile).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Run after deployments.&lt;/strong&gt; Wire the check into your CI/CD pipeline as a post-deploy step. Catch regressions within minutes instead of hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Pair with other monitoring tools.&lt;/strong&gt; Visual monitoring tells you &lt;em&gt;what changed&lt;/em&gt;. Pair it with document extraction tools like &lt;a href="https://parseflow.dev" rel="noopener noreferrer"&gt;ParseFlow&lt;/a&gt; to monitor &lt;em&gt;content&lt;/em&gt; changes (prices, terms, legal text), or with &lt;a href="https://fixmyweb.dev" rel="noopener noreferrer"&gt;FixMyWeb&lt;/a&gt; to catch accessibility regressions alongside visual ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost Estimation
&lt;/h2&gt;

&lt;p&gt;With CaptureAPI's free tier (200 screenshots/month), you can monitor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3 pages x 2 viewports x every 2 hours = ~360 captures/month&lt;/li&gt;
&lt;li&gt;Or 5 pages at desktop-only, checked hourly during business hours = ~220 captures/month&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most small-to-medium projects, the free tier is enough to get started. Paid plans scale up from there.&lt;/p&gt;

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

&lt;p&gt;In about 200 lines of code, we created a system that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Captures screenshots of any URL on a schedule&lt;/li&gt;
&lt;li&gt;Compares them pixel-by-pixel against a baseline&lt;/li&gt;
&lt;li&gt;Generates diff images for debugging&lt;/li&gt;
&lt;li&gt;Sends alerts when visual changes exceed a threshold&lt;/li&gt;
&lt;li&gt;Handles multiple viewports and per-page sensitivity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full source is modular -- you can swap the capture service, add different alerters, or extend the comparison with perceptual hashing for even smarter matching.&lt;/p&gt;

&lt;p&gt;Visual monitoring is one of those tools you don't appreciate until the first time it saves you from shipping a broken page to production. Once it catches your first real bug, you'll wonder how you ever lived without it.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Get 200 free screenshots/month at &lt;a href="https://captureapi.dev" rel="noopener noreferrer"&gt;captureapi.dev&lt;/a&gt;&lt;/strong&gt; to start building your own visual monitoring pipeline.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you set up visual monitoring for your projects? I'd love to hear about your approach in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>api</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why I Built 200+ Digital Templates as a Solo Creator (And What Actually Sells)</title>
      <dc:creator>DevToolsmith</dc:creator>
      <pubDate>Fri, 03 Apr 2026 18:44:51 +0000</pubDate>
      <link>https://dev.to/toolkitonline/why-i-built-200-digital-templates-as-a-solo-creator-and-what-actually-sells-3921</link>
      <guid>https://dev.to/toolkitonline/why-i-built-200-digital-templates-as-a-solo-creator-and-what-actually-sells-3921</guid>
      <description></description>
      <category>productivity</category>
      <category>sideprojects</category>
      <category>beginners</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How Failed Payments Are Silently Killing Your SaaS Revenue (And How to Fix It)</title>
      <dc:creator>DevToolsmith</dc:creator>
      <pubDate>Thu, 02 Apr 2026 22:08:15 +0000</pubDate>
      <link>https://dev.to/toolkitonline/how-failed-payments-are-silently-killing-your-saas-revenue-and-how-to-fix-it-3ed9</link>
      <guid>https://dev.to/toolkitonline/how-failed-payments-are-silently-killing-your-saas-revenue-and-how-to-fix-it-3ed9</guid>
      <description>&lt;h1&gt;
  
  
  How Failed Payments Are Silently Killing Your SaaS Revenue
&lt;/h1&gt;

&lt;p&gt;Here's a number most SaaS founders don't track: &lt;strong&gt;involuntary churn&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not the customers who cancel because they're unhappy. The ones who &lt;em&gt;want to keep paying&lt;/em&gt; but can't — because their credit card expired, hit its limit, or the bank flagged the transaction.&lt;/p&gt;

&lt;p&gt;On average, involuntary churn accounts for &lt;strong&gt;20-40% of total churn&lt;/strong&gt; in subscription businesses. For a SaaS doing $50K MRR, that's $4,500-$9,000 in annual revenue quietly disappearing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Math Behind Failed Payments
&lt;/h2&gt;

&lt;p&gt;Let's break it down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Average card expiration rate: &lt;strong&gt;~25% of cards per year&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Average payment failure rate: &lt;strong&gt;5-10% of renewal attempts&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Average recovery rate without dunning: &lt;strong&gt;~15%&lt;/strong&gt; (banks auto-retry)&lt;/li&gt;
&lt;li&gt;Average recovery rate with dunning: &lt;strong&gt;40-60%&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're not running a dunning process, you're recovering less than a quarter of what you could be.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Real Example
&lt;/h3&gt;

&lt;p&gt;Say you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;500 subscribers at $49/month = $24,500 MRR&lt;/li&gt;
&lt;li&gt;7% monthly payment failure rate = 35 failed payments&lt;/li&gt;
&lt;li&gt;Without dunning: ~5 recovered naturally = $245 saved&lt;/li&gt;
&lt;li&gt;With dunning: ~17 recovered = $833 saved&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monthly difference: $588&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Annual difference: $7,056&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's a senior developer's bonus. From payments your customers already agreed to make.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Payments Fail
&lt;/h2&gt;

&lt;p&gt;The top reasons, based on Stripe's own data:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Insufficient funds&lt;/strong&gt; (30-35%) — Timing issue. Retry in 3-5 days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Card expired&lt;/strong&gt; (25-30%) — Customer needs to update. Email them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bank decline&lt;/strong&gt; (15-20%) — Fraud detection or processing issue. Retry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Card lost/stolen&lt;/strong&gt; (10%) — Customer needs new card. Reach out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Processing error&lt;/strong&gt; (5-10%) — Temporary. Auto-retry usually works.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each reason needs a different recovery strategy. A single "your payment failed" email won't cut it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 3-Step Dunning Sequence That Works
&lt;/h2&gt;

&lt;p&gt;After analyzing recovery patterns across subscription businesses, here's what consistently performs:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Immediate (Day 0)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Subject&lt;/strong&gt;: "Quick heads up — we couldn't process your payment"&lt;/p&gt;

&lt;p&gt;Tone: Helpful, not alarming. Include a direct link to update payment details. Mention the specific plan they're on so it feels personal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recovery rate at this step: 25-35%&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Gentle Follow-up (Day 3)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Subject&lt;/strong&gt;: "Your [Product] access expires soon — update your card"&lt;/p&gt;

&lt;p&gt;Slightly more urgent. Mention what they'll lose access to. Include the amount and next retry date.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cumulative recovery rate: 40-50%&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Final Notice (Day 7)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Subject&lt;/strong&gt;: "Last chance to keep your [Product] account active"&lt;/p&gt;

&lt;p&gt;Direct but respectful. Make it clear this is the final attempt. Offer to help if there's an issue with billing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cumulative recovery rate: 50-60%&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Options
&lt;/h2&gt;

&lt;h3&gt;
  
  
  DIY with Stripe Webhooks
&lt;/h3&gt;

&lt;p&gt;Listen for &lt;code&gt;invoice.payment_failed&lt;/code&gt; events and trigger emails via Resend, SendGrid, or Postmark. Works, but you need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handle retries yourself&lt;/li&gt;
&lt;li&gt;Build the email templates&lt;/li&gt;
&lt;li&gt;Track recovery metrics&lt;/li&gt;
&lt;li&gt;Handle edge cases (multiple failures, plan changes during dunning)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Dedicated Dunning Tools
&lt;/h3&gt;

&lt;p&gt;Tools like &lt;a href="https://paymentrescue.dev" rel="noopener noreferrer"&gt;ChurnGuard&lt;/a&gt; connect via Stripe Connect OAuth and handle the entire dunning flow automatically. Setup takes ~5 minutes.&lt;/p&gt;

&lt;p&gt;The ROI calculator at &lt;a href="https://paymentrescue.dev/calculator" rel="noopener noreferrer"&gt;paymentrescue.dev/calculator&lt;/a&gt; shows exactly how much you're leaving on the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond Email: Advanced Recovery Tactics
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Smart retry timing&lt;/strong&gt; — Don't retry at midnight. Retry when the bank is most likely to approve (morning, weekday, after payday).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Card updater services&lt;/strong&gt; — Stripe's Automatic Card Updating handles some expired cards silently. Make sure it's enabled.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;In-app notifications&lt;/strong&gt; — For active users, show a banner in your app alongside the email.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SMS for high-value accounts&lt;/strong&gt; — For enterprise plans, a text message can cut through inbox noise.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Measuring Recovery Performance
&lt;/h2&gt;

&lt;p&gt;Track these metrics monthly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Payment failure rate&lt;/strong&gt;: Target &amp;lt; 5%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recovery rate&lt;/strong&gt;: Target &amp;gt; 50%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revenue recovered&lt;/strong&gt;: Track in absolute dollars&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time to recovery&lt;/strong&gt;: How quickly you're getting payments back&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're also building APIs, tools like &lt;a href="https://captureapi.dev" rel="noopener noreferrer"&gt;CaptureAPI&lt;/a&gt; can help generate visual reports and dashboards showing your recovery metrics over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How aggressive should dunning emails be?
&lt;/h3&gt;

&lt;p&gt;Never aggressive. These are customers who want to pay you. The tone should be helpful and informative. "Hey, your card didn't go through" not "PAY NOW OR LOSE ACCESS."&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I pause service during dunning?
&lt;/h3&gt;

&lt;p&gt;Best practice: maintain full access during the dunning period (7-14 days). Locking users out immediately increases voluntary churn on top of the involuntary churn.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does dunning work for annual subscriptions?
&lt;/h3&gt;

&lt;p&gt;Absolutely — and it's even more important. A single failed annual payment is 12 months of revenue at risk.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Stop losing revenue to expired credit cards. Check your potential recovery at &lt;a href="https://paymentrescue.dev/calculator" rel="noopener noreferrer"&gt;paymentrescue.dev/calculator&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>saas</category>
      <category>payments</category>
      <category>stripe</category>
      <category>startup</category>
    </item>
    <item>
      <title>EAA Compliance Checklist: 10 Things Every EU Website Must Fix in 2026</title>
      <dc:creator>DevToolsmith</dc:creator>
      <pubDate>Thu, 02 Apr 2026 22:08:11 +0000</pubDate>
      <link>https://dev.to/toolkitonline/eaa-compliance-checklist-10-things-every-eu-website-must-fix-in-2026-32fd</link>
      <guid>https://dev.to/toolkitonline/eaa-compliance-checklist-10-things-every-eu-website-must-fix-in-2026-32fd</guid>
      <description>&lt;h1&gt;
  
  
  EAA Compliance Checklist: 10 Things Every EU Website Must Fix in 2026
&lt;/h1&gt;

&lt;p&gt;The European Accessibility Act (EAA) has been enforced across all 27 EU member states since June 28, 2025. If your website serves European customers, it must meet WCAG 2.1 AA standards — or face fines up to EUR 300,000.&lt;/p&gt;

&lt;p&gt;I've analyzed hundreds of websites and these are the 10 most common accessibility failures, with code-level fixes for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Missing Alt Text on Images
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WCAG 1.1.1 — Non-text Content&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the #1 failure across all websites we scan. Screen readers can't describe images without alt text, making visual content invisible to blind users.&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="c"&gt;&amp;lt;!-- Bad --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"hero.jpg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Good --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"hero.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Team collaborating on accessibility audit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Decorative (intentionally empty) --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"divider.svg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule&lt;/strong&gt;: Every image needs &lt;code&gt;alt&lt;/code&gt;. Decorative images get &lt;code&gt;alt=""&lt;/code&gt; (empty, not missing).&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Insufficient Color Contrast
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WCAG 1.4.3 — Contrast (Minimum)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Light gray text on white backgrounds fails 78% of the sites we scan. The minimum contrast ratios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Normal text (&amp;lt; 18pt): &lt;strong&gt;4.5:1&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Large text (&amp;gt;= 18pt or 14pt bold): &lt;strong&gt;3:1&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;UI components and graphical objects: &lt;strong&gt;3:1&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Bad: 2.5:1 contrast */&lt;/span&gt;
&lt;span class="nt"&gt;color&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="err"&gt;#999&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;background&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;#fff&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c"&gt;/* Good: 7:1 contrast */&lt;/span&gt;
&lt;span class="nt"&gt;color&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="err"&gt;#333&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;background&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;#fff&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Missing Form Labels
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WCAG 1.3.1 — Info and Relationships&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Forms without &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt; elements are unusable for screen reader users. They hear "edit text" with no context.&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="c"&gt;&amp;lt;!-- Bad --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Enter email"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Good --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Email address&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Enter email"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Placeholder text is NOT a substitute for labels. Placeholders disappear when typing.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. No Keyboard Navigation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WCAG 2.1.1 — Keyboard&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All interactive elements must be reachable with Tab key. The biggest culprits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Custom dropdown menus built with &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; instead of &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click handlers on non-focusable elements&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;outline: none&lt;/code&gt; with no visible alternative
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Bad: removes all focus indication */&lt;/span&gt;
&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nd"&gt;:focus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Good: custom but visible focus */&lt;/span&gt;
&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nd"&gt;:focus-visible&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#4A90D9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;outline-offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&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;h2&gt;
  
  
  5. Missing Skip Navigation Link
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WCAG 2.4.1 — Bypass Blocks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Keyboard users shouldn't tab through 50 navigation links on every page. Add a skip link:&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;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#main"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"skip-link"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Skip to main content&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;nav&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;main&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"main"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Auto-Playing Media
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WCAG 1.4.2 — Audio Control&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Auto-playing video or audio disorients screen reader users and can trigger issues for users with cognitive disabilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: Never auto-play. If essential, provide an immediately visible pause button and keep it under 5 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Missing Language Attribute
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WCAG 3.1.1 — Language of Page&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without &lt;code&gt;lang&lt;/code&gt;, screen readers guess the language — often incorrectly, resulting in garbled pronunciation.&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;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- For inline foreign language --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"fr"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Bonjour le monde&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  8. Non-Descriptive Link Text
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WCAG 2.4.4 — Link Purpose&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Screen reader users often navigate by links. "Click here" tells them nothing about the destination.&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="c"&gt;&amp;lt;!-- Bad --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/guide"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Click here&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Good --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/guide"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Read the EAA compliance guide&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  9. Missing Heading Hierarchy
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WCAG 1.3.1 — Info and Relationships&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Screen reader users navigate by headings (H1-H6). Skipping levels breaks this navigation pattern.&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="c"&gt;&amp;lt;!-- Bad: H1 then H4 (skips H2, H3) --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Home&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;h4&amp;gt;&lt;/span&gt;Features&lt;span class="nt"&gt;&amp;lt;/h4&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Good: sequential --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Home&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Features&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;h3&amp;gt;&lt;/span&gt;Feature Detail&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  10. No Accessibility Statement
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;EAA Article 14&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The EAA specifically requires a public accessibility statement on every website. It must include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Current WCAG compliance level (A, AA, or AAA)&lt;/li&gt;
&lt;li&gt;Known limitations and workarounds&lt;/li&gt;
&lt;li&gt;Contact information for accessibility feedback&lt;/li&gt;
&lt;li&gt;Date of last accessibility audit&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check All 10 in 60 Seconds
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://fixmyweb.dev" rel="noopener noreferrer"&gt;FixMyWeb&lt;/a&gt; checks all 10 issues above — plus 191 more — in a single automated scan. The free tier covers 3 scans per month. It also generates an Accessibility Statement automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  EAA Fines by Country
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Country&lt;/th&gt;
&lt;th&gt;Maximum Fine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spain&lt;/td&gt;
&lt;td&gt;EUR 300,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;France&lt;/td&gt;
&lt;td&gt;EUR 250,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ireland&lt;/td&gt;
&lt;td&gt;EUR 200,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Austria&lt;/td&gt;
&lt;td&gt;EUR 200,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Germany&lt;/td&gt;
&lt;td&gt;EUR 100,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Daily fines of approximately EUR 1,000 are possible until issues are resolved.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does the EAA apply to non-EU companies?
&lt;/h3&gt;

&lt;p&gt;Yes. If your website serves EU customers, the EAA applies regardless of where your company is headquartered.&lt;/p&gt;

&lt;h3&gt;
  
  
  What WCAG level does the EAA require?
&lt;/h3&gt;

&lt;p&gt;WCAG 2.1 Level AA is the baseline. Some member states may add additional requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use an overlay widget instead of fixing the code?
&lt;/h3&gt;

&lt;p&gt;Overlays do not provide genuine compliance. Many accessibility experts and organizations advise against them. Fix the underlying code.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Scan your website: &lt;a href="https://fixmyweb.dev" rel="noopener noreferrer"&gt;fixmyweb.dev&lt;/a&gt; — 201 checks, free tier. Also: &lt;a href="https://complipilot.dev" rel="noopener noreferrer"&gt;CompliPilot&lt;/a&gt; for EU AI Act, &lt;a href="https://captureapi.dev" rel="noopener noreferrer"&gt;CaptureAPI&lt;/a&gt; for screenshot API.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>eaa</category>
      <category>compliance</category>
    </item>
    <item>
      <title>How to Build a Website Screenshot API with Node.js and Puppeteer</title>
      <dc:creator>DevToolsmith</dc:creator>
      <pubDate>Wed, 01 Apr 2026 23:28:36 +0000</pubDate>
      <link>https://dev.to/toolkitonline/how-to-build-a-website-screenshot-api-with-nodejs-and-puppeteer-1me0</link>
      <guid>https://dev.to/toolkitonline/how-to-build-a-website-screenshot-api-with-nodejs-and-puppeteer-1me0</guid>
      <description>&lt;p&gt;Need to capture website screenshots programmatically? Here is how to build your own screenshot API using Node.js and Puppeteer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Screenshot APIs?
&lt;/h2&gt;

&lt;p&gt;Common use cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Social media previews&lt;/strong&gt; - Generate OG images from any URL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF reports&lt;/strong&gt; - Convert HTML dashboards to PDF&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual regression testing&lt;/strong&gt; - Catch UI bugs before deploy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt; - Track visual changes on competitor sites&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation&lt;/strong&gt; - Auto-generate screenshots for docs&lt;/li&gt;
&lt;/ul&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;Express&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;Puppeteer&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;sparticuz&lt;/span&gt;&lt;span class="sr"&gt;/chromiu&lt;/span&gt;&lt;span class="err"&gt;m
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: Setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;screenshot-api &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;screenshot-api
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;express puppeteer-core @sparticuz/chromium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Basic Screenshot Endpoint
&lt;/h2&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;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&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;chromium&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@sparticuz/chromium&lt;/span&gt;&lt;span class="dl"&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;puppeteer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;puppeteer-core&lt;/span&gt;&lt;span class="dl"&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/screenshot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;720&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;url&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;URL required&lt;/span&gt;&lt;span class="dl"&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;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;executablePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executablePath&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;headless&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headless&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;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setViewport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;networkidle0&lt;/span&gt;&lt;span class="dl"&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;screenshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&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="s2"&gt;png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Add PDF Generation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// ... browser setup same as above&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pdf&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;h2&gt;
  
  
  Step 4: Deploy to Vercel
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;@sparticuz/chromium-min&lt;/code&gt; for Vercel serverless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @sparticuz/chromium-min
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the bundle under Vercel 50MB limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Or Just Use an API
&lt;/h2&gt;

&lt;p&gt;If you do not want to maintain your own infrastructure, &lt;a href="https://captureapi.dev" rel="noopener noreferrer"&gt;CaptureAPI&lt;/a&gt; handles screenshots, PDFs, and OG images with a single endpoint. Free tier: 200 captures/month.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://captureapi.dev/api/screenshot?url=https://example.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Full source code and docs at &lt;a href="https://captureapi.dev" rel="noopener noreferrer"&gt;captureapi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>api</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title>EU AI Act 2026: What Developers Need to Know</title>
      <dc:creator>DevToolsmith</dc:creator>
      <pubDate>Wed, 01 Apr 2026 23:27:57 +0000</pubDate>
      <link>https://dev.to/toolkitonline/eu-ai-act-2026-what-developers-need-to-know-215i</link>
      <guid>https://dev.to/toolkitonline/eu-ai-act-2026-what-developers-need-to-know-215i</guid>
      <description>&lt;p&gt;The EU AI Act is now law. If you build or deploy AI systems in Europe, here is what you need to know.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the EU AI Act?
&lt;/h2&gt;

&lt;p&gt;The world first comprehensive AI regulation. It classifies AI systems into risk categories and sets requirements for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Risk Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Unacceptable Risk (BANNED)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Social scoring by governments&lt;/li&gt;
&lt;li&gt;Real-time biometric surveillance in public spaces&lt;/li&gt;
&lt;li&gt;AI that manipulates vulnerable groups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;High Risk (STRICT REQUIREMENTS)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Recruitment and HR decisions&lt;/li&gt;
&lt;li&gt;Credit scoring and insurance&lt;/li&gt;
&lt;li&gt;Medical devices and diagnostics&lt;/li&gt;
&lt;li&gt;Critical infrastructure management&lt;/li&gt;
&lt;li&gt;Law enforcement applications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Limited Risk (TRANSPARENCY)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chatbots (must disclose they are AI)&lt;/li&gt;
&lt;li&gt;Deepfake generators (must label content)&lt;/li&gt;
&lt;li&gt;Emotion recognition systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Minimal Risk (NO REQUIREMENTS)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spam filters&lt;/li&gt;
&lt;li&gt;AI in video games&lt;/li&gt;
&lt;li&gt;Inventory management&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Deadlines
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Feb 2025&lt;/strong&gt;: Banned AI practices prohibited&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aug 2025&lt;/strong&gt;: Requirements for general-purpose AI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aug 2026&lt;/strong&gt;: Full enforcement of all provisions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Developers Must Do
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Classify your AI system&lt;/strong&gt; by risk level&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document your training data&lt;/strong&gt; and model decisions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement human oversight&lt;/strong&gt; for high-risk systems&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test for bias&lt;/strong&gt; across protected groups&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Register&lt;/strong&gt; high-risk systems in the EU database&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Penalties
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Up to 35M EUR or 7% of global revenue for banned AI&lt;/li&gt;
&lt;li&gt;Up to 15M EUR or 3% for high-risk non-compliance&lt;/li&gt;
&lt;li&gt;Up to 7.5M EUR or 1.5% for providing incorrect info&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick Compliance Check
&lt;/h2&gt;

&lt;p&gt;Not sure where your AI system falls? &lt;a href="https://complipilot.dev" rel="noopener noreferrer"&gt;CompliPilot&lt;/a&gt; scans your system against 200+ EU AI Act requirements automatically. Free tier available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://artificialintelligenceact.eu/" rel="noopener noreferrer"&gt;EU AI Act full text&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://digital-strategy.ec.europa.eu/en/policies/european-approach-artificial-intelligence" rel="noopener noreferrer"&gt;EC AI Office&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Building AI in Europe? Start with a free compliance check at &lt;a href="https://complipilot.dev" rel="noopener noreferrer"&gt;complipilot.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>europe</category>
      <category>compliance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>15 Free Online Tools Every Developer Needs in 2026</title>
      <dc:creator>DevToolsmith</dc:creator>
      <pubDate>Wed, 01 Apr 2026 23:25:13 +0000</pubDate>
      <link>https://dev.to/toolkitonline/15-free-online-tools-every-developer-needs-in-2026-2lid</link>
      <guid>https://dev.to/toolkitonline/15-free-online-tools-every-developer-needs-in-2026-2lid</guid>
      <description>&lt;p&gt;Whether you are debugging JSON, generating passwords, or converting files - here are 15 free tools that save me hours every week. All browser-based, no signup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Text and Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. JSON Formatter&lt;/strong&gt; - Paste messy JSON, get it formatted. &lt;a href="https://toolkitonline.vip/en/tools/json-formatter" rel="noopener noreferrer"&gt;Try it&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Regex Tester&lt;/strong&gt; - Write and test regex with real-time highlighting. &lt;a href="https://toolkitonline.vip/en/tools/regex-tester" rel="noopener noreferrer"&gt;Try it&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Base64 Converter&lt;/strong&gt; - Encode/decode Base64. &lt;a href="https://toolkitonline.vip/en/tools/base64-converter" rel="noopener noreferrer"&gt;Try it&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Markdown Preview&lt;/strong&gt; - Live preview with GFM support. &lt;a href="https://toolkitonline.vip/en/tools/markdown-preview" rel="noopener noreferrer"&gt;Try it&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Security
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;5. Password Generator&lt;/strong&gt; - Cryptographically secure. &lt;a href="https://toolkitonline.vip/en/tools/password-generator" rel="noopener noreferrer"&gt;Try it&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Hash Generator&lt;/strong&gt; - MD5, SHA-1, SHA-256, SHA-512. &lt;a href="https://toolkitonline.vip/en/tools/hash-generator" rel="noopener noreferrer"&gt;Try it&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conversion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;7. CSV to JSON&lt;/strong&gt; - Handles headers and nested data. &lt;a href="https://toolkitonline.vip/en/tools/csv-to-json" rel="noopener noreferrer"&gt;Try it&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. URL Encoder&lt;/strong&gt; - Essential for debugging query params. &lt;a href="https://toolkitonline.vip/en/tools/url-encoder" rel="noopener noreferrer"&gt;Try it&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Hex Converter&lt;/strong&gt; - Hex, decimal, binary, octal. &lt;a href="https://toolkitonline.vip/en/tools/hex-converter" rel="noopener noreferrer"&gt;Try it&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Design
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;10. Color Picker&lt;/strong&gt; - HEX/RGB/HSL conversion. &lt;a href="https://toolkitonline.vip/en/tools/color-picker" rel="noopener noreferrer"&gt;Try it&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;11. Color Palette Generator&lt;/strong&gt; - Harmonious palettes. &lt;a href="https://toolkitonline.vip/en/tools/color-palette-generator" rel="noopener noreferrer"&gt;Try it&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Images
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;12. Image Compressor&lt;/strong&gt; - Lossless compression. &lt;a href="https://toolkitonline.vip/en/tools/image-compressor" rel="noopener noreferrer"&gt;Try it&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;13. QR Code Generator&lt;/strong&gt; - URLs, text, contact info. &lt;a href="https://toolkitonline.vip/en/tools/qr-code-generator" rel="noopener noreferrer"&gt;Try it&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Productivity
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;14. Pomodoro Timer&lt;/strong&gt; - Custom work/break intervals. &lt;a href="https://toolkitonline.vip/en/tools/pomodoro-timer" rel="noopener noreferrer"&gt;Try it&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;15. Cron Expression Generator&lt;/strong&gt; - Visual cron builder. &lt;a href="https://toolkitonline.vip/en/tools/cron-expression-generator" rel="noopener noreferrer"&gt;Try it&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All 143+ tools at &lt;a href="https://toolkitonline.vip" rel="noopener noreferrer"&gt;toolkitonline.vip&lt;/a&gt;. What free tools do you use daily?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tools</category>
      <category>productivity</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I Built 5 SaaS Products in 7 Days Using AI</title>
      <dc:creator>DevToolsmith</dc:creator>
      <pubDate>Wed, 01 Apr 2026 22:53:57 +0000</pubDate>
      <link>https://dev.to/toolkitonline/i-built-5-saas-products-in-7-days-using-ai-3km9</link>
      <guid>https://dev.to/toolkitonline/i-built-5-saas-products-in-7-days-using-ai-3km9</guid>
      <description>&lt;p&gt;From zero to five live SaaS products in one week. Here is what I learned, what broke, and what I would do differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;I wanted to test: can one developer, armed with Claude and Next.js, ship real products in a week?&lt;/p&gt;

&lt;p&gt;The answer: yes, but with caveats.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5 Products
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AccessiScan&lt;/strong&gt; (fixmyweb.dev) - WCAG accessibility scanner, 201 checks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CaptureAPI&lt;/strong&gt; (captureapi.dev) - Screenshot + PDF generation API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CompliPilot&lt;/strong&gt; (complipilot.dev) - EU AI Act compliance scanner&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ChurnGuard&lt;/strong&gt; (paymentrescue.dev) - Failed payment recovery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DocuMint&lt;/strong&gt; (parseflow.dev) - PDF to JSON parsing API&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All built with Next.js, TypeScript, Tailwind, deployed on Vercel.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Worked
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AI for boilerplate code (auth, API routes, UI components)&lt;/li&gt;
&lt;li&gt;Vercel for instant deployment&lt;/li&gt;
&lt;li&gt;Upstash Redis for rate limiting and usage tracking&lt;/li&gt;
&lt;li&gt;Stripe for payments (surprisingly easy to integrate)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Did Not Work
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Trying to make everything perfect before shipping&lt;/li&gt;
&lt;li&gt;Building features nobody asked for&lt;/li&gt;
&lt;li&gt;Spending too long on design before validating demand&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Numbers (Honest)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Product&lt;/th&gt;
&lt;th&gt;Pages&lt;/th&gt;
&lt;th&gt;Build Time&lt;/th&gt;
&lt;th&gt;Revenue&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AccessiScan&lt;/td&gt;
&lt;td&gt;40+&lt;/td&gt;
&lt;td&gt;8h&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CaptureAPI&lt;/td&gt;
&lt;td&gt;40+&lt;/td&gt;
&lt;td&gt;6h&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CompliPilot&lt;/td&gt;
&lt;td&gt;40+&lt;/td&gt;
&lt;td&gt;10h&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChurnGuard&lt;/td&gt;
&lt;td&gt;45+&lt;/td&gt;
&lt;td&gt;12h&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DocuMint&lt;/td&gt;
&lt;td&gt;40+&lt;/td&gt;
&lt;td&gt;8h&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Yes, zero revenue so far. Building is the easy part. Finding customers is the hard part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Ship fast, iterate based on feedback&lt;/li&gt;
&lt;li&gt;AI accelerates coding 3-5x but you still need to understand what you are building&lt;/li&gt;
&lt;li&gt;The European Accessibility Act creates real demand for accessibility tools&lt;/li&gt;
&lt;li&gt;Payment recovery is a real problem - 30 percent of SaaS revenue is lost to failed payments&lt;/li&gt;
&lt;li&gt;Distribution matters more than product quality&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I Would Do Differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Start with one product, not five&lt;/li&gt;
&lt;li&gt;Find 10 potential customers BEFORE building&lt;/li&gt;
&lt;li&gt;Use cold email outreach from day one&lt;/li&gt;
&lt;li&gt;Focus on SEO content from the start&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All products are live with free tiers. Try them out and let me know what you think!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building in public at toolkitonline.vip&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>nextjs</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Extract Structured Data from PDFs Without ML Training</title>
      <dc:creator>DevToolsmith</dc:creator>
      <pubDate>Wed, 01 Apr 2026 16:08:37 +0000</pubDate>
      <link>https://dev.to/toolkitonline/how-to-extract-structured-data-from-pdfs-without-ml-training-5b35</link>
      <guid>https://dev.to/toolkitonline/how-to-extract-structured-data-from-pdfs-without-ml-training-5b35</guid>
      <description>&lt;p&gt;Extracting data from PDFs usually means training ML models, setting up OCR, or manual data entry. All expensive and slow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://parseflow.dev" rel="noopener noreferrer"&gt;DocuMint&lt;/a&gt; does it in one API call. Upload a PDF, get structured JSON back.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Extracts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Invoices&lt;/strong&gt;: number, date, vendor, line items, totals&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Receipts&lt;/strong&gt;: merchant, items, total, payment method&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contracts&lt;/strong&gt;: parties, dates, terms&lt;/li&gt;
&lt;/ul&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Free Tier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DocuMint&lt;/td&gt;
&lt;td&gt;$19/mo&lt;/td&gt;
&lt;td&gt;100 pages/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docparser&lt;/td&gt;
&lt;td&gt;$39/mo&lt;/td&gt;
&lt;td&gt;50 pages/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parseur&lt;/td&gt;
&lt;td&gt;$39/mo&lt;/td&gt;
&lt;td&gt;20 pages/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most affordable option with the best free tier.&lt;/p&gt;

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

&lt;p&gt;The &lt;a href="https://parseflow.dev/playground" rel="noopener noreferrer"&gt;playground&lt;/a&gt; lets you test with your own PDFs. No signup needed.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Also: &lt;a href="https://fixmyweb.dev" rel="noopener noreferrer"&gt;AccessiScan&lt;/a&gt; for accessibility, &lt;a href="https://complipilot.dev" rel="noopener noreferrer"&gt;CompliPilot&lt;/a&gt; for AI Act, &lt;a href="https://captureapi.dev" rel="noopener noreferrer"&gt;CaptureAPI&lt;/a&gt; for screenshots&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>pdf</category>
      <category>automation</category>
      <category>devtools</category>
    </item>
    <item>
      <title>EU AI Act: 4 Months Left to Comply - What Developers Need to Know</title>
      <dc:creator>DevToolsmith</dc:creator>
      <pubDate>Wed, 01 Apr 2026 16:08:08 +0000</pubDate>
      <link>https://dev.to/toolkitonline/eu-ai-act-4-months-left-to-comply-what-developers-need-to-know-4d5c</link>
      <guid>https://dev.to/toolkitonline/eu-ai-act-4-months-left-to-comply-what-developers-need-to-know-4d5c</guid>
      <description>&lt;p&gt;The EU AI Act deadline for high-risk AI systems is August 2, 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Penalties
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Violation&lt;/th&gt;
&lt;th&gt;Max Fine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prohibited AI&lt;/td&gt;
&lt;td&gt;35M euros or 7% turnover&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-risk&lt;/td&gt;
&lt;td&gt;15M euros or 3% turnover&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Risk management&lt;/li&gt;
&lt;li&gt;Data governance&lt;/li&gt;
&lt;li&gt;Technical documentation&lt;/li&gt;
&lt;li&gt;Transparency&lt;/li&gt;
&lt;li&gt;Human oversight&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://complipilot.dev" rel="noopener noreferrer"&gt;CompliPilot&lt;/a&gt; runs 200+ automated checks. Free 3 scans/month.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Also: &lt;a href="https://fixmyweb.dev" rel="noopener noreferrer"&gt;AccessiScan&lt;/a&gt;, &lt;a href="https://captureapi.dev" rel="noopener noreferrer"&gt;CaptureAPI&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>regulation</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Why You Should Stop Self-Hosting Puppeteer for Screenshots</title>
      <dc:creator>DevToolsmith</dc:creator>
      <pubDate>Wed, 01 Apr 2026 16:08:07 +0000</pubDate>
      <link>https://dev.to/toolkitonline/why-you-should-stop-self-hosting-puppeteer-for-screenshots-bii</link>
      <guid>https://dev.to/toolkitonline/why-you-should-stop-self-hosting-puppeteer-for-screenshots-bii</guid>
      <description>&lt;p&gt;Self-hosting Puppeteer in production is painful: memory leaks, 400MB Docker images, Chromium crashes.&lt;/p&gt;

&lt;p&gt;In 2026, screenshot APIs cost less than the server time debugging Puppeteer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;Free&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;PDF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CaptureAPI&lt;/td&gt;
&lt;td&gt;200/mo&lt;/td&gt;
&lt;td&gt;$9/mo&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ScreenshotOne&lt;/td&gt;
&lt;td&gt;100/mo&lt;/td&gt;
&lt;td&gt;$9/mo&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Urlbox&lt;/td&gt;
&lt;td&gt;Trial&lt;/td&gt;
&lt;td&gt;$49/mo&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://captureapi.dev" rel="noopener noreferrer"&gt;CaptureAPI&lt;/a&gt; does screenshots + PDFs + OG images in one endpoint. 200 free/month.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Also: &lt;a href="https://fixmyweb.dev" rel="noopener noreferrer"&gt;AccessiScan&lt;/a&gt;, &lt;a href="https://complipilot.dev" rel="noopener noreferrer"&gt;CompliPilot&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>puppeteer</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
