<?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>Forem: reynaldi</title>
    <description>The latest articles on Forem by reynaldi (@reynaldi).</description>
    <link>https://forem.com/reynaldi</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%2F542591%2F3868269d-a0fd-45d6-b46c-46208bdc0559.jpeg</url>
      <title>Forem: reynaldi</title>
      <link>https://forem.com/reynaldi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/reynaldi"/>
    <language>en</language>
    <item>
      <title>How to Set the Host Header in Fetch (JavaScript)</title>
      <dc:creator>reynaldi</dc:creator>
      <pubDate>Fri, 27 Feb 2026 04:24:55 +0000</pubDate>
      <link>https://forem.com/reynaldi/how-to-set-the-host-header-in-fetch-javascript-agc</link>
      <guid>https://forem.com/reynaldi/how-to-set-the-host-header-in-fetch-javascript-agc</guid>
      <description>&lt;p&gt;The &lt;code&gt;Host&lt;/code&gt; header is a forbidden header name in the browser, which means JavaScript cannot set it using &lt;code&gt;fetch()&lt;/code&gt; or &lt;code&gt;XMLHttpRequest&lt;/code&gt;. If you need to control the &lt;code&gt;Host&lt;/code&gt; header for API integrations or web scraping, the solution is to route the request through a server-side proxy that can set the header for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Solution
&lt;/h2&gt;

&lt;p&gt;Use Corsfix to set the &lt;code&gt;Host&lt;/code&gt; header from your frontend code. Pass the header you want inside &lt;code&gt;x-corsfix-headers&lt;/code&gt;, and Corsfix will apply it server-side before forwarding your request.&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://proxy.corsfix.com/?https://api.example.com/data&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="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="s2"&gt;x-corsfix-headers&lt;/span&gt;&lt;span class="dl"&gt;"&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="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api.example.com&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="nf"&gt;then&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="o"&gt;=&amp;gt;&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;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Corsfix receives your request, sets the &lt;code&gt;Host&lt;/code&gt; header to the value you specified, and forwards it to the target URL. The response is then returned to your browser with the proper CORS headers.&lt;/p&gt;

&lt;p&gt;For local development, this works instantly without registration. For live websites, &lt;a href="https://app.corsfix.com/applications" rel="noopener noreferrer"&gt;set up your domain&lt;/a&gt; (takes 30 seconds).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Browsers Block the Host Header
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Host&lt;/code&gt; header is part of the browser's &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name" rel="noopener noreferrer"&gt;forbidden header names&lt;/a&gt; list. This is a set of headers that the browser manages automatically and doesn't allow JavaScript to modify. The &lt;code&gt;Host&lt;/code&gt; header tells the server which domain the request is intended for, and browsers set it based on the URL you're requesting.&lt;/p&gt;

&lt;p&gt;This is purely a browser-level restriction. In server-side environments like Node.js, you have full control over all headers including &lt;code&gt;Host&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Node.js — this works fine&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com/data&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="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="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;custom-host.example.com&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Browser — this is silently ignored&lt;/span&gt;
&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com/data&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="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="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;custom-host.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Browser ignores this&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;In the browser, the &lt;code&gt;Host&lt;/code&gt; header you set is silently ignored and the browser uses the hostname from the URL instead. That's where a proxy comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a Proxy Solves the Problem
&lt;/h2&gt;

&lt;p&gt;Since browsers won't let you set the &lt;code&gt;Host&lt;/code&gt; header directly, the workaround is to let a proxy do it for you.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your browser sends a request to Corsfix with the target URL and the headers you want to override inside &lt;code&gt;x-corsfix-headers&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Corsfix reads the override instructions, sets the actual &lt;code&gt;Host&lt;/code&gt; header on the outbound request, and forwards it to the target API.&lt;/li&gt;
&lt;li&gt;The target API receives the request with the &lt;code&gt;Host&lt;/code&gt; header value you specified.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When Do You Need to Set the Host Header?
&lt;/h2&gt;

&lt;p&gt;There are a few real-world scenarios where controlling the &lt;code&gt;Host&lt;/code&gt; header matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Virtual hosting&lt;/strong&gt; — the target server hosts multiple domains on the same IP address and uses the &lt;code&gt;Host&lt;/code&gt; header to route requests to the correct site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API gateways&lt;/strong&gt; — some API gateways validate the &lt;code&gt;Host&lt;/code&gt; header and reject requests where it doesn't match the expected value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web scraping&lt;/strong&gt; — certain websites check the &lt;code&gt;Host&lt;/code&gt; header as part of their bot-detection logic and block requests with unexpected values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SNI and TLS routing&lt;/strong&gt; — servers behind load balancers may use the &lt;code&gt;Host&lt;/code&gt; header to determine which TLS certificate to serve.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Only Override What You Need
&lt;/h3&gt;

&lt;p&gt;Don't override the &lt;code&gt;Host&lt;/code&gt; header on every request. Only use it when the target API specifically requires a particular &lt;code&gt;Host&lt;/code&gt; value. In most cases, the default behavior (using the hostname from the URL) is correct.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handle Errors Gracefully
&lt;/h3&gt;

&lt;p&gt;When the &lt;code&gt;Host&lt;/code&gt; header doesn't match what the server expects, you may get unexpected errors:&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;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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://proxy.corsfix.com/?https://api.example.com/data&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="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="s2"&gt;x-corsfix-headers&lt;/span&gt;&lt;span class="dl"&gt;"&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="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;custom-host.example.com&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="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="c1"&gt;// Common errors when Host is misconfigured:&lt;/span&gt;
  &lt;span class="c1"&gt;// 403 Forbidden — server rejected the Host value&lt;/span&gt;
  &lt;span class="c1"&gt;// 421 Misdirected Request — Host doesn't match the server&lt;/span&gt;
  &lt;span class="c1"&gt;// 502 Bad Gateway — upstream server couldn't route the request&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;`Request 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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Use a server-side proxy like &lt;a href="https://corsfix.com/" rel="noopener noreferrer"&gt;Corsfix&lt;/a&gt; to set the &lt;code&gt;Host&lt;/code&gt; header in JavaScript fetch. Since browsers restrict the &lt;code&gt;Host&lt;/code&gt; header as a forbidden header name, a proxy is the simplest way to override it without setting up your own backend.&lt;/p&gt;

&lt;p&gt;Corsfix handles the &lt;code&gt;Host&lt;/code&gt; header override (along with other &lt;a href="https://corsfix.com/override-request-header" rel="noopener noreferrer"&gt;forbidden headers&lt;/a&gt;) server-side, so you can focus on building your app instead of managing proxy infrastructure. It's free to get started, and you only need to upgrade when you go to production.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>cors</category>
    </item>
    <item>
      <title>Building a File Sharing App with Puter.js</title>
      <dc:creator>reynaldi</dc:creator>
      <pubDate>Tue, 24 Feb 2026 08:45:16 +0000</pubDate>
      <link>https://forem.com/reynaldi/building-a-file-sharing-app-with-puterjs-2i37</link>
      <guid>https://forem.com/reynaldi/building-a-file-sharing-app-with-puterjs-2i37</guid>
      <description>&lt;p&gt;In this tutorial, we'll build a file sharing app that lets you upload any file and instantly get a shareable public URL using &lt;a href="https://docs.puter.com" rel="noopener noreferrer"&gt;Puter.js&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdeveloper.puter.com%2Fassets%2Fimg%2Fupload-share.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdeveloper.puter.com%2Fassets%2Fimg%2Fupload-share.webp" alt="Building a File Sharing App with Puter.js" width="800" height="491"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt; &lt;a href="https://file-sharing-example.puter.site/" rel="noopener noreferrer"&gt;https://file-sharing-example.puter.site/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Features
&lt;/h2&gt;

&lt;p&gt;Our file sharing app provides a simple drag-and-drop interface for sharing files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Drag &amp;amp; drop upload&lt;/strong&gt;: Select files by dragging them onto the page or clicking to browse&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud storage&lt;/strong&gt;: Files are stored in Puter's cloud file system&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public URLs&lt;/strong&gt;: Each uploaded file gets a public URL via Puter Hosting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent subdomain&lt;/strong&gt;: The app remembers your subdomain across sessions using Puter KV&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;This app is a single &lt;code&gt;index.html&lt;/code&gt; file with no build step. Add Puter.js via CDN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://js.puter.com/v2/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Puter.js is a JavaScript library that provides features like &lt;a href="https://dev.to/ai/"&gt;AI&lt;/a&gt;, &lt;a href="https://dev.to/object-storage/"&gt;cloud file storage&lt;/a&gt;, &lt;a href="https://dev.to/key-value-database/"&gt;database&lt;/a&gt;, &lt;a href="https://dev.to/hosting/"&gt;hosting&lt;/a&gt;, and authentication, all running client-side with a &lt;a href="https://docs.puter.com/user-pays-model/" rel="noopener noreferrer"&gt;User-Pays Model&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the App
&lt;/h2&gt;

&lt;p&gt;The app includes a drag-and-drop UI, file previews, and progress indicators. This tutorial focuses on the Puter.js APIs that power it. You can find the &lt;a href="https://github.com/Puter-Apps/file-sharing" rel="noopener noreferrer"&gt;full source code here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When the user selects a file and clicks upload, three things happen using Puter.js:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a hosted subdomain&lt;/strong&gt; to serve files publicly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload the file&lt;/strong&gt; to Puter's cloud file system&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate a public link&lt;/strong&gt; from the subdomain and file name&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Creating a Hosted Subdomain
&lt;/h3&gt;

&lt;p&gt;Before uploading, we need a place to serve the files. Puter Hosting lets you create a subdomain under &lt;code&gt;*.puter.site&lt;/code&gt; that maps to your cloud file system. We create one on first use and save it with &lt;code&gt;puter.kv&lt;/code&gt; so it persists across sessions:&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;// Check if we already have a subdomain from a previous session&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;subdomain&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;puter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kv&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;site_subdomain&lt;/span&gt;&lt;span class="dl"&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;subdomain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// First time: generate a unique subdomain and create the site&lt;/span&gt;
  &lt;span class="nx"&gt;subdomain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&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;puter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hosting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subdomain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&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;puter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;site_subdomain&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;subdomain&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;code&gt;puter.hosting.create(subdomain, directory)&lt;/code&gt; links the subdomain to a directory in Puter's file system. Here we point it at &lt;code&gt;"."&lt;/code&gt; (the app root), so any file we write to will be accessible at &lt;code&gt;https://&amp;lt;subdomain&amp;gt;.puter.site/&amp;lt;filename&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;puter.kv&lt;/code&gt; acts as a simple key-value database. By saving the subdomain here, subsequent visits skip the creation step and reuse the same subdomain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Uploading the File
&lt;/h3&gt;

&lt;p&gt;With the subdomain ready, we write the file to Puter's cloud file system. To avoid name collisions, we prefix the file name with a short UUID:&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;// Generate a unique file name to avoid collisions&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;extension&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;selectedFile&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="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;selectedFile&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="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uniqueName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;extension&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Write the file to Puter's cloud file system&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;puter&lt;/span&gt;&lt;span class="p"&gt;.&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;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uniqueName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedFile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;puter.fs.write(path, content)&lt;/code&gt; writes any &lt;code&gt;File&lt;/code&gt;, &lt;code&gt;Blob&lt;/code&gt;, or string to the user's cloud storage. Since our hosted site points at the root directory, this file is now publicly accessible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generating the Public Link
&lt;/h3&gt;

&lt;p&gt;The public URL is simply the subdomain plus the file name:&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;publicUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;subdomain&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.puter.site/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uniqueName&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use &lt;code&gt;encodeURIComponent&lt;/code&gt; on the file name to handle any special characters in the extension or UUID. The user can now copy this URL and share it with anyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You've now built a file sharing app with Puter.js by combining:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Puter File System&lt;/strong&gt;: Storing uploaded files in the cloud with &lt;code&gt;puter.fs.write&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Puter Hosting&lt;/strong&gt;: Creating a public &lt;code&gt;*.puter.site&lt;/code&gt; subdomain to serve files with &lt;code&gt;puter.hosting.create&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Puter Key-Value Store&lt;/strong&gt;: Persisting the subdomain across sessions with &lt;code&gt;puter.kv&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best part? With Puter's &lt;a href="https://docs.puter.com/user-pays-model/" rel="noopener noreferrer"&gt;User-Pays Model&lt;/a&gt;, you can deploy this app for free. Users authenticate with their own Puter account, so they cover their own usage, making your app free to run and scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt; &lt;a href="https://file-sharing-example.puter.site/" rel="noopener noreferrer"&gt;https://file-sharing-example.puter.site/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source Code:&lt;/strong&gt; &lt;a href="https://github.com/Puter-Apps/file-sharing" rel="noopener noreferrer"&gt;https://github.com/Puter-Apps/file-sharing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Get started with &lt;a href="https://docs.puter.com/" rel="noopener noreferrer"&gt;Puter.js&lt;/a&gt; and access features like AI, cloud storage, key-value stores, hosting, and more.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>puter</category>
    </item>
    <item>
      <title>Building an AI-Powered RAG Application with Puter.js</title>
      <dc:creator>reynaldi</dc:creator>
      <pubDate>Fri, 06 Feb 2026 13:24:14 +0000</pubDate>
      <link>https://forem.com/reynaldi/building-an-ai-powered-rag-application-with-puterjs-1j9l</link>
      <guid>https://forem.com/reynaldi/building-an-ai-powered-rag-application-with-puterjs-1j9l</guid>
      <description>&lt;p&gt;In this tutorial, we'll build a RAG (Retrieval-Augmented Generation) application that lets you chat with any website using &lt;a href="https://docs.puter.com" rel="noopener noreferrer"&gt;Puter.js&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdeveloper.puter.com%2Fassets%2Fimg%2Fstampy.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdeveloper.puter.com%2Fassets%2Fimg%2Fstampy.jpg" alt="Building an AI-Powered RAG Application with Puter.js" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt; &lt;a href="https://stampy.puter.site/" rel="noopener noreferrer"&gt;https://stampy.puter.site/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Features
&lt;/h2&gt;

&lt;p&gt;Our RAG application allows you to chat with any website, with answers grounded in the actual website content:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add any website&lt;/strong&gt;: Enter a sitemap URL to index an entire site&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval&lt;/strong&gt;: Full-text search across all indexed pages using MiniSearch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generation&lt;/strong&gt;: AI-powered chat with function calling via Puter AI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What is RAG?&lt;/strong&gt; RAG combines information retrieval with AI text generation. Instead of relying solely on an AI model's training data, RAG first searches through relevant documents, then uses that context to generate accurate, grounded responses. This means the AI can answer questions about specific content it was never trained on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Set up your project with any JavaScript framework you prefer (React, Vue, Svelte, or even vanilla JS), then add Puter.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install @heyputer/puter.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or, via CDN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;script src="https://js.puter.com/v2/"&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Puter.js is a JavaScript library that provides features like &lt;a href="https://developer.puter.com/ai/" rel="noopener noreferrer"&gt;AI&lt;/a&gt;, &lt;a href="https://developer.puter.com/object-storage/" rel="noopener noreferrer"&gt;cloud file storage&lt;/a&gt;, &lt;a href="https://developer.puter.com/key-value-database/" rel="noopener noreferrer"&gt;database&lt;/a&gt;, &lt;a href="https://developer.puter.com/networking/" rel="noopener noreferrer"&gt;networking&lt;/a&gt;, and authentication, all running client-side with a &lt;a href="https://docs.puter.com/user-pays-model/" rel="noopener noreferrer"&gt;User-Pays Model&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can find the full &lt;a href="https://docs.puter.com/" rel="noopener noreferrer"&gt;Puter.js documentation here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the App
&lt;/h2&gt;

&lt;p&gt;Our RAG app works in three stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Crawl &amp;amp; Store&lt;/strong&gt;: Fetch all pages from a website's sitemap, extract the text content, and store it in Puter's cloud file system. Site metadata is persisted in Puter's key-value database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Index for Search&lt;/strong&gt;: Build a search index from the stored documents so we can quickly find relevant content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Chat with Function Calling&lt;/strong&gt;: Let the AI decide when to search the documents and use the results to generate grounded answers&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Crawling the Website
&lt;/h3&gt;

&lt;p&gt;We start by fetching the website's sitemap, an XML file that lists all pages on a site. This gives us a list of URLs to crawl:&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;// Fetch the sitemap using Puter's networking&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="nx"&gt;puter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&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;sitemapUrl&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;sitemapContent&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Parse URLs from the sitemap XML&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseSitemapUrls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;xmlContent&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;parser&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;DOMParser&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;xmlDoc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;xmlContent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/xml&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;locElements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;xmlDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;urlset url loc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Array&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="nx"&gt;locElements&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;loc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&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="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseSitemapUrls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sitemapContent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once we have all the URLs, we fetch each page and extract the text content. To save tokens and help the AI understand the page content, we strip away all the markup and keep just the meaningful text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchHTMLContent&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="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="nx"&gt;puter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&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;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&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;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseHTMLContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parser&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;DOMParser&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;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/html&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;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;body&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;urlObj&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;URL&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&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;urlObj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;urlObj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;#content.txt`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Crawl all pages and build the documents array&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;documents&lt;/span&gt; &lt;span class="o"&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;url&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;urls&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;html&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;fetchHTMLContent&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseHTMLContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we store each document in Puter's cloud file system. This gives us persistent storage without needing to set up a database or backend server. The documents are saved to the user's Puter account and will be available across sessions:&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="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;doc&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;documents&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="nx"&gt;puter&lt;/span&gt;&lt;span class="p"&gt;.&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;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;createMissingParents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Managing Sites with Puter Key-Value Store
&lt;/h3&gt;

&lt;p&gt;While Puter's file system stores the actual document content, we need a way to keep track of which sites have been indexed. Puter's &lt;a href="https://developer.puter.com/key-value-database/" rel="noopener noreferrer"&gt;key-value store&lt;/a&gt; (&lt;code&gt;puter.kv&lt;/code&gt;) works as a database for this: we store a list of site entries, each containing the site name, hostname, sitemap URL, and the path to its search index.&lt;/p&gt;

&lt;p&gt;Loading the list of indexed sites is straightforward:&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;websites&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;puter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kv&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;websites&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;When a user adds a new site (after crawling and indexing), we save the site entry with its metadata:&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;hostname&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sitemapUrl&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;hostname&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;newSite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sitemap_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sitemapUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;index_path&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;hostname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/index.json`&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;updatedSites&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;sites&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newSite&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;puter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;websites&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;updatedSites&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdeveloper.puter.com%2Fassets%2Fimg%2Fstampy-website.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdeveloper.puter.com%2Fassets%2Fimg%2Fstampy-website.webp" alt="Adding website to our Puter.js RAG application" width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Deleting a site removes it from both the key-value store and the file system:&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;// Remove the site's files from Puter FS&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;puter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Update the site list in Puter KV&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updatedSites&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sites&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="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;puter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;websites&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;updatedSites&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation keeps things clean: &lt;code&gt;puter.fs&lt;/code&gt; handles the heavy content storage, while &lt;code&gt;puter.kv&lt;/code&gt; acts as a database for site metadata. Both persist to the user's Puter account, so everything is available across sessions without any backend setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding Search for Website Content
&lt;/h3&gt;

&lt;p&gt;At this point, we have document content in Puter FS and site metadata in Puter KV. But to answer user questions, we still need a way to quickly find the right documents. This is the "Retrieval" in RAG: before the AI generates an answer, we first retrieve the most relevant documents to give it context.&lt;/p&gt;

&lt;p&gt;We use &lt;a href="https://lucaong.github.io/minisearch/" rel="noopener noreferrer"&gt;MiniSearch&lt;/a&gt;, a lightweight full-text search library that runs entirely in the browser. A search index works like a book's index: instead of scanning every page to find a word, you look it up and jump straight to the relevant pages. This makes searching thousands of documents nearly instant:&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;// Create and populate the search index from the crawled documents&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;miniSearch&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;MiniSearch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;fields&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="s2"&gt;title&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="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;// Fields to index&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;miniSearch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Serialize and store the index in Puter FS&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hostname&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sitemapUrl&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;hostname&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;searchIndexPath&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;hostname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/index.json`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;miniSearch&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;puter&lt;/span&gt;&lt;span class="p"&gt;.&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;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchIndexPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;searchIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;createMissingParents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;When a user selects a site and asks a question, we load its saved index and search for relevant documents. MiniSearch returns results ranked by relevance, so we take the top matches to use as context for the AI:&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;// Load the saved index from Puter FS using the site's index path&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;indexBlob&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;puter&lt;/span&gt;&lt;span class="p"&gt;.&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;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index_path&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;indexData&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;indexBlob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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;miniSearch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;MiniSearch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;indexData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;fields&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="s2"&gt;title&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="s2"&gt;text&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;// Search and get top results&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;miniSearch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;topResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Adding AI Chat with Function Calling
&lt;/h3&gt;

&lt;p&gt;This is where everything comes together. We use Puter AI with &lt;a href="https://docs.puter.com/AI/chat/#function-calling" rel="noopener noreferrer"&gt;&lt;strong&gt;function calling&lt;/strong&gt;&lt;/a&gt;, a feature that lets the AI decide when to search the documents rather than us hardcoding when to search.&lt;/p&gt;

&lt;p&gt;Function calling works by giving the AI a list of "tools" it can use. We describe what each tool does, and the AI autonomously decides when to call them based on the conversation. For our RAG app, we define a search tool that the AI will call whenever it needs information from the indexed website:&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;tools&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;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;function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;function&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="s2"&gt;search_documents&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Search through website documents to find relevant information&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;parameters&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="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;query&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="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The search query to find relevant documents&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="na"&gt;required&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="s2"&gt;query&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chat flow works in two steps. First, we send the user's question to the AI along with our tool definitions. The AI analyzes the question and decides whether it needs to search for information. If it does, it returns a "tool call" instead of a direct answer. We then execute that search, feed the results back to the AI, and let it generate a final response grounded in the actual document content:&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;systemPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Answer in the context of the document. Search documents before clarifying with users.&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;// Build conversation from system prompt and user messages&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;conversationMessages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;userMessages&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// First call: AI decides whether to search&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="nx"&gt;puter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;conversationMessages&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="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openrouter:google/gemini-2.5-flash-lite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;stream&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="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// If AI called the search tool, execute it&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;response&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="nx"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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;toolCall&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;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&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;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolCall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Execute search and fetch document contents&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;miniSearch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&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="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&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;documentContents&lt;/span&gt; &lt;span class="o"&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;result&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;searchResults&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;puter&lt;/span&gt;&lt;span class="p"&gt;.&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;read&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;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;documentContents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&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;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Add tool result to conversation&lt;/span&gt;
  &lt;span class="nx"&gt;conversationMessages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tool_call_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;toolCall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;documentContents&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\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="c1"&gt;// Second call: Generate final response with context&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finalResponse&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;puter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;conversationMessages&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="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openrouter:google/gemini-2.5-flash-lite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Stream for better UX&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;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdeveloper.puter.com%2Fassets%2Fimg%2Fstampy-chat.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdeveloper.puter.com%2Fassets%2Fimg%2Fstampy-chat.webp" alt="AI answering question with relevant information" width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You've now built an AI-powered RAG application with Puter.js by incorporating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Puter Networking&lt;/strong&gt;: Fetching website content without CORS issues&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Puter File System&lt;/strong&gt;: Storing document content and search indexes in the cloud&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Puter Key-Value Store&lt;/strong&gt;: Managing site metadata as a database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MiniSearch&lt;/strong&gt;: Full-text search for document retrieval&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Puter AI&lt;/strong&gt;: Chat with function calling for intelligent document search&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best part? With Puter's &lt;a href="https://docs.puter.com/user-pays-model/" rel="noopener noreferrer"&gt;User-Pays Model&lt;/a&gt;, you can deploy this app for free. Users authenticate with their own Puter account, so they cover their own usage, making your app free to run and scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt; &lt;a href="https://stampy.puter.site/" rel="noopener noreferrer"&gt;https://stampy.puter.site/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source Code:&lt;/strong&gt; &lt;a href="https://github.com/Puter-Apps/stampy" rel="noopener noreferrer"&gt;https://github.com/Puter-Apps/stampy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Get started with &lt;a href="https://docs.puter.com/" rel="noopener noreferrer"&gt;Puter.js&lt;/a&gt; and access features like AI, cloud storage, key-value stores, and more.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>ai</category>
      <category>puter</category>
    </item>
    <item>
      <title>Why Most APIs Don't Enable CORS?</title>
      <dc:creator>reynaldi</dc:creator>
      <pubDate>Mon, 13 Oct 2025 10:50:00 +0000</pubDate>
      <link>https://forem.com/reynaldi/why-most-apis-dont-enable-cors-2mp2</link>
      <guid>https://forem.com/reynaldi/why-most-apis-dont-enable-cors-2mp2</guid>
      <description>&lt;p&gt;You might be wondering why some APIs don't enable &lt;a href="https://corsfix.com/blog/cors" rel="noopener noreferrer"&gt;CORS&lt;/a&gt;, and what exactly they're trying to protect. After all, what's the problem in calling it from a browser?&lt;/p&gt;

&lt;p&gt;In this article, we'll explore the reasons why APIs don't enable CORS, debunk some common misconceptions, and look at how you can ethically work with these APIs when you need client-side access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Most APIs Don't Enable CORS
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Server-to-Server Communication by Design
&lt;/h3&gt;

&lt;p&gt;Many APIs are specifically designed for server-to-server communication. Usage in the browser wasn't part of the original plan, and the API makers want to maintain. By not enabling CORS, they can assert control over how their API is consumed, ensuring it's only called from server-side.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preventing API Token Leakage
&lt;/h3&gt;

&lt;p&gt;When an API requires authentication tokens, having those tokens on the client side creates a security risk. Developers might mistakenly include tokens directly in frontend code, where they become visible to anyone who views the source or inspects network requests. By disabling CORS, API providers push developers to use server-side implementations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rate Limiting and Access Control
&lt;/h3&gt;

&lt;p&gt;Having an API only accessible without CORS means that typically only servers can call it, compared to allowing it to be called freely on any website. This makes rate limiting much more manageable for several reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Easier to identify clients&lt;/strong&gt;: Server-side requests are easier to track and attribute to specific users or organizations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduced volume&lt;/strong&gt;: Without client-side access, there's inherently less request volume since each user isn't making direct API calls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better enforcement&lt;/strong&gt;: Rate limits can be more effectively enforced when requests come from a controlled number of server origins&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What It's NOT Protecting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  CORS Is Not A Protection For Session Hijacking
&lt;/h3&gt;

&lt;p&gt;While some sources claim that disabling CORS protects against credential theft or session hijacking, this is misleading at best. &lt;strong&gt;CORS is not a security mechanism, it's an access control mechanism&lt;/strong&gt;, as the name itself suggests: &lt;a href="https://corsfix.com/cors-headers/access-control-allow-origin" rel="noopener noreferrer"&gt;Access-Control-Allow-Origin&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to protect against credential theft and CSRF attacks, you need to use different mechanisms entirely, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cookie attributes like &lt;code&gt;HttpOnly&lt;/code&gt;, &lt;code&gt;Secure&lt;/code&gt;, and &lt;code&gt;SameSite=Lax&lt;/code&gt; or &lt;code&gt;SameSite=Strict&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;CSRF tokens&lt;/li&gt;
&lt;li&gt;Proper authentication headers instead of cookie-based auth for APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These mechanisms prevent credentials from being included in unauthorized cross-origin requests. CORS simply controls whether a browser allows your JavaScript to read a response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Does a CORS Proxy Fit Here?
&lt;/h2&gt;

&lt;p&gt;A &lt;a href="https://corsfix.com/" rel="noopener noreferrer"&gt;CORS proxy like Corsfix&lt;/a&gt; allows you to call and fetch APIs directly from the client side while still being respectful of the API provider's intentions. Here's how:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Server-to-Server Communication Requirements&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Corsfix provides an easy method for developers to call these APIs while still conforming to the requirement of server-side execution, your requests go through a server (the proxy) rather than directly from the browser.&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;// GET request&lt;/span&gt;
&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://proxy.corsfix.com/?https://api.example.com/users&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="nf"&gt;then&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="o"&gt;=&amp;gt;&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;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;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="nx"&gt;data&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;For Preventing API Token Exposure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Corsfix has first-class support for &lt;a href="https://corsfix.com/docs/cors-proxy/secrets-variable" rel="noopener noreferrer"&gt;secrets&lt;/a&gt;, allowing you to securely store tokens and use them in your requests without exposing them in client-side code:&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;// Instead of exposing your token in the frontend:&lt;/span&gt;
&lt;span class="c1"&gt;// ❌ Don't do this&lt;/span&gt;
&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com/data&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="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="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bearer sk-123...&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;// ✅ Use Corsfix with secrets variable&lt;/span&gt;
&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://proxy.corsfix.com/?&amp;lt;url&amp;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="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="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bearer {{SECRET_TOKEN}}&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures you never expose secrets while still having access to a wide range of APIs from your frontend application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Rate Limiting Concerns&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The target API can still limit throughput for requests coming through the proxy, which is important for maintaining their service quality. Additionally, Corsfix provides &lt;a href="https://corsfix.com/docs/cors-proxy/cached-response" rel="noopener noreferrer"&gt;caching features&lt;/a&gt; that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reduce load on the target server by serving cached responses when appropriate&lt;/li&gt;
&lt;li&gt;Provide better performance to end users since cached requests return instantly&lt;/li&gt;
&lt;li&gt;Help you stay within rate limits by reducing redundant API calls
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://proxy.corsfix.com/?&amp;lt;url&amp;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="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="s2"&gt;x-corsfix-cache&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="s2"&gt;true&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;APIs don't enable CORS for reasons such as: to enforce server-to-server communication patterns, prevent API token leakage, and maintain rate limiting.&lt;/p&gt;

&lt;p&gt;It's important to understand that &lt;strong&gt;CORS isn't a security protection mechanism, it's an access control mechanism&lt;/strong&gt;, as clearly indicated by its name.&lt;/p&gt;

&lt;p&gt;When you need to call these APIs from client-side code, you can use a CORS proxy like Corsfix.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>cors</category>
    </item>
    <item>
      <title>Building an HTML-to-Image Website Screenshot Tool</title>
      <dc:creator>reynaldi</dc:creator>
      <pubDate>Wed, 01 Oct 2025 10:29:12 +0000</pubDate>
      <link>https://forem.com/reynaldi/building-an-html-to-image-website-screenshot-tool-20g7</link>
      <guid>https://forem.com/reynaldi/building-an-html-to-image-website-screenshot-tool-20g7</guid>
      <description>&lt;p&gt;Most website screenshot tools require backend infrastructure and setting up browser instances, but you can build one entirely in the frontend using modern JavaScript libraries and a CORS proxy.&lt;/p&gt;

&lt;p&gt;In this article you will learn how to build a website screenshot tool from scratch directly in the frontend.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9xohluf3v9s02guek3ou.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9xohluf3v9s02guek3ou.jpg" alt="Website Screenshot Tool" width="800" height="375"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Website Screenshot Tool&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Live demo: &lt;a href="https://corsfix.com/tools/website-screenshot" rel="noopener noreferrer"&gt;https://corsfix.com/tools/website-screenshot&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How Screenshot Tools Typically Work
&lt;/h2&gt;

&lt;p&gt;Most screenshot tools you see online use backend systems like Puppeteer, where there's an actual server running a headless browser instance to capture screenshots.&lt;/p&gt;

&lt;p&gt;This approach works well because it shows accurate screenshots as if someone opened the page in their browser. The rendering engine handles JavaScript, CSS, and fonts exactly as a real browser would.&lt;/p&gt;

&lt;p&gt;But there's an overhead, as you need server infrastructure, and you're running entire browser instances just to take screenshots, as well as you have to manage scaling and resource allocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternative: In-Browser HTML to Image
&lt;/h2&gt;

&lt;p&gt;Website screenshots are fundamentally about converting a page's visual elements, which are its HTML structure and CSS styling, into an image.&lt;/p&gt;

&lt;p&gt;You'd think there should be a way to render and convert HTML and CSS directly into an image without spinning up a browser instance, and you'd be right.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/zumerlab/snapdom" rel="noopener noreferrer"&gt;snapDOM&lt;/a&gt; is a JavaScript library that converts HTML to images directly in the browser. It's more up-to-date and faster than its predecessors like &lt;a href="https://github.com/niklasvh/html2canvas" rel="noopener noreferrer"&gt;html2canvas&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx6ydi5ne2f6nfn35obm5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx6ydi5ne2f6nfn35obm5.jpg" alt="snapDOM" width="800" height="608"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;snapDOM&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting the website HTML
&lt;/h2&gt;

&lt;p&gt;We have the first part figured out, which is snapDOM for HTML-to-image conversion. Next, thing to figure out is how to get the actual HTML to input in our library.&lt;/p&gt;

&lt;p&gt;We need the actual HTML content from the target website. But directly fetching HTML from another domain results in CORS errors, where browsers block cross-origin requests.&lt;/p&gt;

&lt;p&gt;This is where &lt;a href="https://corsfix.com" rel="noopener noreferrer"&gt;Corsfix&lt;/a&gt; comes in. It's a &lt;a href="https://corsfix.com/blog/cors-proxy" rel="noopener noreferrer"&gt;CORS proxy&lt;/a&gt; that lets you fetch HTML from any domain without CORS errors, making it possible to get the content you need directly from the browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpiubzzmhxjvm16d8x2qw.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpiubzzmhxjvm16d8x2qw.jpg" alt="Corsfix CORS proxy" width="800" height="420"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Corsfix CORS proxy&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;The implementation flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fetch the HTML&lt;/strong&gt; using Corsfix to bypass CORS restrictions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render the HTML&lt;/strong&gt; using iframe as container&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let Snapdom process&lt;/strong&gt; the HTML, render it with styles and fonts, and produce the image
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified example&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com&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;proxyUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://proxy.corsfix.com/?url=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&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;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Fetch HTML through proxy&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="nx"&gt;proxyUrl&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;html&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Render in iframe&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iframe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;iframe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;iframe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;srcdoc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iframe&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Convert to image with Snapdom&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;snapdom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toImg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iframe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the complete demo: &lt;a href="https://corsfix.com/tools/website-screenshot" rel="noopener noreferrer"&gt;https://corsfix.com/tools/website-screenshot&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You can build a website screenshot tool using &lt;a href="https://corsfix.com" rel="noopener noreferrer"&gt;Corsfix&lt;/a&gt; for fetching HTML content and snapDOM for HTML-to-image conversion. This eliminates the need for backend servers and browser instances.&lt;/p&gt;

&lt;p&gt;Solve CORS errors instantly with Corsfix, it's &lt;a href="https://app.corsfix.com" rel="noopener noreferrer"&gt;free to start&lt;/a&gt; and you only upgrade when moving to production.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>cors</category>
    </item>
    <item>
      <title>How Much Bandwidth Do You Need for an API in 2025?</title>
      <dc:creator>reynaldi</dc:creator>
      <pubDate>Sun, 10 Aug 2025 03:50:32 +0000</pubDate>
      <link>https://forem.com/reynaldi/how-much-bandwidth-do-you-need-for-an-api-in-2025-440p</link>
      <guid>https://forem.com/reynaldi/how-much-bandwidth-do-you-need-for-an-api-in-2025-440p</guid>
      <description>&lt;p&gt;When talking about bandwidth or data transfer in the context of the web, most people think of bandwidth used by the actual webpages that you host. Thanks to a report by &lt;a href="https://httparchive.org/reports/page-weight" rel="noopener noreferrer"&gt;HTTP Archive&lt;/a&gt;, we know that webpages in 2025 weigh around 2.5 MB.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fejr8lop62nf3ujfei15g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fejr8lop62nf3ujfei15g.png" alt="Page Weight" width="800" height="456"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Page Weight (source: HTTP Archive)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This page weight includes everything that gets downloaded to render a page, like HTML, CSS, JS, fonts, images, and other assets.&lt;/p&gt;

&lt;p&gt;However, this does not tell the story about bandwidth used for APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Websites Are Heavy
&lt;/h2&gt;

&lt;p&gt;It's tempting to assume that HTML, CSS, and JS are tiny since they are just text. However, HTML itself is bloated since you need to use markup tags, not including the attributes and more inline data.&lt;/p&gt;

&lt;p&gt;Add to that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CSS frameworks that load hundreds of KB&lt;/li&gt;
&lt;li&gt;JavaScript bundles&lt;/li&gt;
&lt;li&gt;Images, assets, fonts&lt;/li&gt;
&lt;li&gt;Tracking scripts, analytics, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So it's not a surprise that when talking about websites, they are in the MB range.&lt;/p&gt;

&lt;h2&gt;
  
  
  APIs Are a Different Story
&lt;/h2&gt;

&lt;p&gt;There's no equivalent report for APIs similar to HTTP Archive for webpages. However, we can make some educated estimates.&lt;/p&gt;

&lt;p&gt;APIs typically send structured data, mostly in JSON format, without extra markup, styles, or images. To put this into perspective, here is a &lt;a href="https://microsoftedge.github.io/Demos/json-dummy-data/128KB.json" rel="noopener noreferrer"&gt;128 KB dummy JSON data&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftwhz7mwd2y9ae7iem4eu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftwhz7mwd2y9ae7iem4eu.png" alt="128 KB JSON" width="800" height="418"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;128 KB JSON&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It contains over 700+ objects, each with multiple fields. In most scenarios, an API response in this size range (100KB) is already considered large.&lt;/p&gt;

&lt;h2&gt;
  
  
  Estimate: APIs Are 20x Lighter Than Webpages
&lt;/h2&gt;

&lt;p&gt;If we compare:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Webpage&lt;/td&gt;
&lt;td&gt;~2.5MB&lt;/td&gt;
&lt;td&gt;HTML, CSS, JS, images, assets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;~100 KB&lt;/td&gt;
&lt;td&gt;Mostly text JSON data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's a 20x difference. If a webpage consumes that amount of bandwidth, an API request would likely be 5% of that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means for Bandwidth Planning
&lt;/h2&gt;

&lt;p&gt;For a website with heavy assets, total data transfer can easily reach hundreds of gigabytes or even terabytes per month at scale. This is why you see website hosting providers offer a lot of bandwidth compared to API services.&lt;/p&gt;

&lt;p&gt;APIs, however, are much lighter. So your data transfer costs and bandwidth needs for APIs are often much lower than for a full website, even with the same number of requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;APIs are &lt;strong&gt;significantly lighter&lt;/strong&gt; compared to webpages. They can be up to 20x lighter than websites, even with the same number of requests. When planning for bandwidth needs or data transfer usage with APIs, you can expect to use much less data compared to your web hosting requirements.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>api</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Building a Real-Time Forex Rate Conversion App with Finage API</title>
      <dc:creator>reynaldi</dc:creator>
      <pubDate>Wed, 30 Jul 2025 12:48:24 +0000</pubDate>
      <link>https://forem.com/reynaldi/building-a-real-time-forex-rate-conversion-app-with-finage-api-505n</link>
      <guid>https://forem.com/reynaldi/building-a-real-time-forex-rate-conversion-app-with-finage-api-505n</guid>
      <description>&lt;p&gt;In the world of international finance and travel, having access to real-time currency exchange rates is essential. The &lt;a href="https://finage.co.uk" rel="noopener noreferrer"&gt;Finage API&lt;/a&gt; provides reliable and up-to-date forex data that you can integrate into your applications.&lt;/p&gt;

&lt;p&gt;In this article, you'll learn what it takes to build a modern, static-first, responsive forex conversion application. The app will feature real-time currency conversion, and a clean user interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://forex-conversion.pages.dev" rel="noopener noreferrer"&gt;Live Demo&lt;/a&gt;&lt;/strong&gt; | &lt;strong&gt;&lt;a href="https://github.com/corsfix/forex-conversion" rel="noopener noreferrer"&gt;Source Code&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What's in our Forex Conversion App?&lt;/li&gt;
&lt;li&gt;Setting Up The Project&lt;/li&gt;
&lt;li&gt;Building the Application&lt;/li&gt;
&lt;li&gt;Key Features Explained&lt;/li&gt;
&lt;li&gt;Deploying the Application&lt;/li&gt;
&lt;li&gt;Final Thoughts&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. What's in our Forex Conversion App?
&lt;/h2&gt;

&lt;p&gt;Our forex conversion app will include these key features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Convert between major currencies (USD, EUR, GBP, JPY, CAD, AUD, CHF, CNY)&lt;/li&gt;
&lt;li&gt;Real-time exchange rates powered by Finage API&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Setting Up The Project
&lt;/h2&gt;

&lt;p&gt;In order to have access to the real-time forex data, you will need to create an account and obtain your API key at &lt;a href="https://finage.co.uk" rel="noopener noreferrer"&gt;Finage&lt;/a&gt;. This key will be used to authenticate your requests to the Finage API and fetch live exchange rates.&lt;/p&gt;

&lt;p&gt;We will be configuring our application for static export. This allows the application to be deployed to any static hosting platform without requiring a server. This makes deployment simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Building the Application
&lt;/h2&gt;

&lt;p&gt;To get live exchange rates, we'll fetch data from the Finage API. Here's how to call the forex endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchExchangeRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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="nx"&gt;apiKey&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;NEXT_PUBLIC_API_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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://api.finage.co.uk/convert/forex/&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="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/1?apikey=&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="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="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&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="k"&gt;return&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;rates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;to&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;More information about the Finage API can be found in their &lt;a href="https://finage.co.uk/docs" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However, when building a static web application, we'll encounter CORS (Cross-Origin Resource Sharing) issues when trying to call API directly from the browser. This is where &lt;a href="https://corsfix.com" rel="noopener noreferrer"&gt;Corsfix CORS proxy&lt;/a&gt; comes to the rescue. It allows us to send request directly from the browser without any server setup.&lt;/p&gt;

&lt;p&gt;Instead of calling the API directly, we prefix the URL with the Corsfix proxy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchExchangeRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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="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;NEXT_PUBLIC_API_KEY&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{{FINAGE_API_KEY}}&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://api.finage.co.uk/convert/forex/&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="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/1?apikey=&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="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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://proxy.corsfix.com/?&lt;/span&gt;&lt;span class="dl"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&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="k"&gt;return&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;rates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;to&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;We will also store our secret securely in Corsfix, so you can use the app without exposing your API key in the client JavaScript code. Learn more about how to &lt;a href="https://corsfix.com/docs/cors-proxy/secrets-variable" rel="noopener noreferrer"&gt;secure your API key with Corsfix&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Key Features Explained
&lt;/h2&gt;

&lt;p&gt;As you type in any amount, the app automatically converts to the other currency, ensuring a smooth user experience. It works by fetching the exchange rate from the Finage API and applying it to the input value. The conversion happens in real-time with the interface updating immediately to show the converted amount with proper formatting and currency symbols.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Deploying the Application
&lt;/h2&gt;

&lt;p&gt;Since the app is configured for static export, it can be deployed to any static hosting service without server requirements. Popular hosting platforms like Cloudflare Pages, Vercel, Netlify, and GitHub Pages all support static deployments with automatic builds from your Git repository. The deployment process is typically as simple as connecting your repository and the platform will handle the build and deployment automatically whenever you push changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Final Thoughts
&lt;/h2&gt;

&lt;p&gt;In this article, you've learned what it takes to build a comprehensive forex conversion application that demonstrates several important concepts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API Integration&lt;/strong&gt;: Using the Finage API for real-time forex data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CORS Solutions&lt;/strong&gt;: Leveraging Corsfix to overcome browser limitations in static apps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The resulting application provides a smooth, professional user experience while demonstrating best practices for handling external APIs in static web applications. See the complete source code on &lt;a href="https://github.com/corsfix/forex-conversion" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>cors</category>
    </item>
    <item>
      <title>All-in-One Resource: CORS Headers Explained</title>
      <dc:creator>reynaldi</dc:creator>
      <pubDate>Wed, 04 Jun 2025 09:53:13 +0000</pubDate>
      <link>https://forem.com/reynaldi/all-in-one-resource-cors-headers-explained-3gn8</link>
      <guid>https://forem.com/reynaldi/all-in-one-resource-cors-headers-explained-3gn8</guid>
      <description>&lt;p&gt;I'm excited to introduce &lt;a href="https://corsfix.com/cors-headers" rel="noopener noreferrer"&gt;CORS Headers Explained&lt;/a&gt;, an all-in-one resource that explains CORS headers with usage examples, common errors, and solutions. If you've ever been confused by CORS errors or struggled to implement CORS correctly, this resource is for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why CORS Headers Explained?
&lt;/h2&gt;

&lt;p&gt;Understanding &lt;a href="https://dev.to/blog/cors"&gt;CORS&lt;/a&gt; headers is essential for modern web development, but the official documentation can be overwhelming and scattered. Many developers know they need to set CORS headers but aren't sure which ones to use or how they work together. CORS Headers Explained aims to simplify this by providing a clear, structured guide to every CORS header you need to know.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You'll Find
&lt;/h2&gt;

&lt;p&gt;Our resource covers every CORS header you need to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complete Header Reference:&lt;/strong&gt; Explanations of all CORS headers including &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;, &lt;code&gt;Access-Control-Allow-Methods&lt;/code&gt;, &lt;code&gt;Access-Control-Allow-Headers&lt;/code&gt;, and more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practical Code Examples:&lt;/strong&gt; Implementations examples for server and client-side code for every header.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Common Error Solutions:&lt;/strong&gt; Clear solutions for errors and fixes for the most frequent CORS issues.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each header page includes syntax examples, usage examples, common error solutions, and related headers to help you understand how everything fits together.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use This Resource
&lt;/h2&gt;

&lt;p&gt;Whether you're setting up CORS for the first time, debugging a tricky preflight request, or need to understand what each header does, CORS Headers Explained is your go-to reference. It's perfect for both beginners learning CORS fundamentals and experienced developers who need quick answers.&lt;/p&gt;

&lt;p&gt;Visit &lt;a href="https://corsfix.com/cors-headers" rel="noopener noreferrer"&gt;CORS Headers Explained&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;Source code is available on &lt;a href="https://github.com/corsfix/cors-headers" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>cors</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Enabling CORS in Flask</title>
      <dc:creator>reynaldi</dc:creator>
      <pubDate>Thu, 22 May 2025 17:00:00 +0000</pubDate>
      <link>https://forem.com/reynaldi/enabling-cors-in-flask-120n</link>
      <guid>https://forem.com/reynaldi/enabling-cors-in-flask-120n</guid>
      <description>&lt;p&gt;When you’re building a full-stack app with a separate frontend and a Flask backend, the browser’s &lt;strong&gt;Same-Origin Policy&lt;/strong&gt; will block your API calls unless you explicitly allow them. Cross-Origin Resource Sharing (CORS) is the protocol that lets you opt-in. In this guide we will cover two common ways to get it done in Flask.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding CORS headers manually
&lt;/h2&gt;

&lt;p&gt;Flask lets you set response headers directly, so you can choose which endpoints get which CORS rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Basic allow-origin
&lt;/h3&gt;

&lt;p&gt;If your frontend lives at &lt;code&gt;https://myapp.com&lt;/code&gt;, you can allow it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;make_response&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hello&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;msg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="c1"&gt;# allow just one origin
&lt;/span&gt;    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://myapp.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Handling the pre-flight (OPTIONS) request
&lt;/h3&gt;

&lt;p&gt;Whenever the browser thinks the real call might be “unsafe” (custom headers, non-GET method, etc.) it sends an &lt;strong&gt;OPTIONS&lt;/strong&gt; probe first. You have to answer it with the correct headers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPTIONS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hello&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPTIONS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://myapp.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Methods&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET, POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type, X-Custom&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Credentials&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# only if you need cookies / auth
&lt;/span&gt;        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;204&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;

    &lt;span class="c1"&gt;# Normal GET/POST handler
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;msg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="c1"&gt;# allow CORS for actual response
&lt;/span&gt;    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://myapp.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Credentials&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Manual is great when you need per-endpoint logic (e.g., allow some origins only for certain users), but it gets repetitive fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the &lt;code&gt;flask-cors&lt;/code&gt; library
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;flask-cors&lt;/code&gt; library will handle the header logic for you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;flask-cors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Global configuration (one-liner)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask_cors&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CORS&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;CORS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;origins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://myapp.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;supports_credentials&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every route responds with the right CORS headers and the pre-flight dance “just works.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Fine-grained control
&lt;/h3&gt;

&lt;p&gt;You can apply it to a specific blueprint or route instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask_cors&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cross_origin&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/public&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@cross_origin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                      &lt;span class="c1"&gt;# allow all origins
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;public_stuff&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/private&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@cross_origin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;origins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://myapp.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
              &lt;span class="n"&gt;expose_headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Total-Count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;private_stuff&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key options you’ll use most:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;origins&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Accept single origin, list, or "*"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;methods&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Restrict allowed verbs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;allow_headers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extra request headers you expect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expose_headers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Response headers the browser can read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;supports_credentials&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;true&lt;/code&gt; → cookies/auth tokens allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;To enable CORS in your Flask project, either manually add CORS headers to specific endpoints, or use a third-party library like &lt;code&gt;flask-cors&lt;/code&gt; to manage CORS more easily. Either way, both options will allow you to build a robust full-stack application.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>flask</category>
      <category>python</category>
      <category>beginners</category>
    </item>
    <item>
      <title>What are CORS Safelisted Headers?</title>
      <dc:creator>reynaldi</dc:creator>
      <pubDate>Thu, 22 May 2025 11:51:38 +0000</pubDate>
      <link>https://forem.com/reynaldi/what-are-cors-safelisted-headers-1nam</link>
      <guid>https://forem.com/reynaldi/what-are-cors-safelisted-headers-1nam</guid>
      <description>&lt;p&gt;A big part of understanding CORS is knowing about CORS safelisted headers. These are the special headers that the browser treats differently when it comes to cross-origin requests. Knowing how they work can save you headaches when debugging CORS issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are CORS Safelisted Headers?
&lt;/h2&gt;

&lt;p&gt;CORS safelisted headers are split into two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Safelisted request headers: these allow requests to be made to a different origin without a preflight OPTIONS request.&lt;/li&gt;
&lt;li&gt;Safelisted response headers: these are headers that browsers expose to JavaScript on cross-origin responses by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CORS Safelisted Request Headers
&lt;/h2&gt;

&lt;p&gt;CORS safelisted request headers are those that you can include in a cross-origin request without triggering a preflight &lt;code&gt;OPTIONS&lt;/code&gt; request. A preflight request is a preliminary request sent by the browser to check if the actual request is allowed by the server. This means you can use them without worrying about the server needing to respond to an OPTIONS request first.&lt;/p&gt;

&lt;p&gt;The safelisted request headers are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Accept&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Accept-Language&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Content-Language&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Content-Type&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Range&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And specifically for &lt;code&gt;Content-Type&lt;/code&gt;, the only values that are allowed without triggering a preflight request are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;multipart/form-data&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;text/plain&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you send another value, like &lt;code&gt;application/json&lt;/code&gt;, the browser will send a preflight request first to check if the server allows that content type.&lt;/p&gt;

&lt;h2&gt;
  
  
  CORS Safelisted Response Headers
&lt;/h2&gt;

&lt;p&gt;CORS safelisted response headers, also known as simple response headers, are those that the browser exposes to JavaScript by default. This means you can access them in your JavaScript code without needing to do anything special.&lt;/p&gt;

&lt;p&gt;The safelisted response headers are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Cache-Control&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Content-Language&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Content-Length&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Content-Type&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Expires&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Last-Modified&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Pragma&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By default, other response headers are not exposed to JavaScript. If you want to expose additional headers, the server must include the &lt;code&gt;Access-Control-Expose-Headers&lt;/code&gt; header in its response. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Access-Control-Expose-Headers: X-Custom-Header, X-Another-Custom-Header
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;CORS safelisted headers consist of the safelisted request headers and the safelisted response headers. The request headers are those that you can use in a cross-origin request without triggering a preflight request, while the response headers are those that the browser exposes to JavaScript by default. Understanding these headers is crucial for working with CORS and ensuring that your cross-origin requests work as expected.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>cors</category>
    </item>
    <item>
      <title>How to Fetch Files From a GitHub Release (Without CORS Errors)</title>
      <dc:creator>reynaldi</dc:creator>
      <pubDate>Tue, 06 May 2025 07:51:39 +0000</pubDate>
      <link>https://forem.com/reynaldi/how-to-fetch-files-from-a-github-release-without-cors-errors-2oc7</link>
      <guid>https://forem.com/reynaldi/how-to-fetch-files-from-a-github-release-without-cors-errors-2oc7</guid>
      <description>&lt;p&gt;You’re building a web app and need to grab a ZIP or PNG straight from a GitHub release. You send a &lt;code&gt;fetch()&lt;/code&gt; from the browser, but run into a &lt;strong&gt;CORS error&lt;/strong&gt;. Let’s see why that happens and how you can fix it in two straightforward ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s the Problem?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Redirects break the pre-flight
&lt;/h3&gt;

&lt;p&gt;The download URL GitHub gives you is just a 302 that bounces to an S3 blob. CORS pre-flights (&lt;code&gt;OPTIONS&lt;/code&gt; requests) don’t follow redirects, so the browser stops right there.&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="nt"&gt;-I&lt;/span&gt; https://github.com/OWNER/REPO/releases/download/TAG/file.zip
 &lt;span class="c"&gt;# ... HTTP/2 302 Location: https://objects.githubusercontent.com/...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. No CORS headers on the final asset
&lt;/h3&gt;

&lt;p&gt;Even if you follow the redirect manually, the S3 file itself doesn’t send &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;, so the browser still refuses to hand over the bytes.&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="nt"&gt;-I&lt;/span&gt; &lt;span class="s1"&gt;'https://objects.githubusercontent.com/.../file.zip'&lt;/span&gt;
 &lt;span class="c"&gt;# HTTP/2 200 (but no Access-Control-Allow-Origin)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bottom line: GitHub’s release CDN isn’t set up for direct browser fetches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing GitHub Release CORS Errors
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Relay Through Your Backend
&lt;/h3&gt;

&lt;p&gt;Already have a server? Relay the request via your backend:&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;// Express example&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;/asset&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="nx"&gt;upstream&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://github.com/OWNER/REPO/releases/download/TAG/file.zip&lt;/span&gt;&lt;span class="dl"&gt;"&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;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="nx"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&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;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&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;send&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="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;upstream&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your frontend now calls the relay endpoint, the server handles the redirect, and the browser never sees a CORS issue.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use a CORS Proxy (No Backend Needed)
&lt;/h3&gt;

&lt;p&gt;No server? Drop a CORS proxy in front of the GitHub URL, it follows the redirect and injects the right headers.&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://proxy.corsfix.com/?https://github.com/OWNER/REPO/releases/download/TAG/file.zip&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Either route gives you a clean, CORS-friendly response in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;GitHub release assets trigger CORS errors because the initial redirect breaks pre-flight checks and the final file lacks the needed headers. You can solve this by relaying the request through your own backend for full control or by inserting a CORS proxy for the quickest setup. Pick the approach that fits your stack and move on without CORS errors.&lt;/p&gt;

&lt;p&gt;Need production-ready &lt;a href="https://corsfix.com" rel="noopener noreferrer"&gt;CORS proxy&lt;/a&gt;? Give Corsfix a try, it’s free to start and only upgrade when you go to production.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>cors</category>
    </item>
    <item>
      <title>Tainted Canvas: why the browser blocks your canvas (and how to unblock it)</title>
      <dc:creator>reynaldi</dc:creator>
      <pubDate>Wed, 30 Apr 2025 10:02:55 +0000</pubDate>
      <link>https://forem.com/reynaldi/tainted-canvas-why-the-browser-blocks-your-canvas-and-how-to-unblock-it-57e7</link>
      <guid>https://forem.com/reynaldi/tainted-canvas-why-the-browser-blocks-your-canvas-and-how-to-unblock-it-57e7</guid>
      <description>&lt;p&gt;Working with the &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; feels simple at first, load an image, draw it, export it. But the moment that image lives on a different origin you hit an error: calls such as &lt;code&gt;canvas.toDataURL()&lt;/code&gt; or &lt;code&gt;getImageData()&lt;/code&gt; shows error with “Tainted canvases may not be exported.” The browser is preventing you from reading the pixels of that canvas.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flacwnp9bi4hksyt5pijn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flacwnp9bi4hksyt5pijn.png" alt="Tainted canvas error" width="800" height="207"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Tainted canvas error&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What’s with the tainted canvas?
&lt;/h2&gt;

&lt;p&gt;When you draw an image fetched from another origin without proper CORS setup, the browser makes the entire canvas as “tainted.” Any attempt to read those pixels (&lt;code&gt;getImageData&lt;/code&gt;, &lt;code&gt;toBlob&lt;/code&gt;, &lt;code&gt;toDataURL&lt;/code&gt;, etc.) is then blocked.&lt;/p&gt;

&lt;p&gt;To solve this, you need to add &lt;code&gt;crossOrigin="anonymous"&lt;/code&gt; to asks the browser to send the request in CORS mode, but the server must still reply with a matching &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; header.&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;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/image.png"&lt;/span&gt; &lt;span class="na"&gt;crossorigin=&lt;/span&gt;&lt;span class="s"&gt;"anonymous"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Solving the tainted canvas error
&lt;/h2&gt;

&lt;p&gt;You’ve got two options to solve the tainted canvas error:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Control the server?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Add the header &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; (or your domain) and keep &lt;code&gt;crossOrigin="anonymous"&lt;/code&gt; on your &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag or &lt;code&gt;drawImage&lt;/code&gt; source.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Don’t control the server?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend relay:&lt;/strong&gt; Fetch the image in your own backend, then serve it with the right CORS header so the browser sees it as same-origin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CORS proxy:&lt;/strong&gt; Use a &lt;a href="https://corsfix.com/" rel="noopener noreferrer"&gt;CORS proxy&lt;/a&gt; that injects the header for you, ideal if you are running a static only site or don't have a backend.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&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;image&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;Image&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crossOrigin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anonymous&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://proxy.corsfix.com/?https://example.com/image.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canvas&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;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&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;dataUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDataURL&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="nx"&gt;dataUrl&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;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A tainted canvas is just the browser’s way of preventing you from accessing a canvas that contains cross-origin data. Tell the browser to expect CORS (&lt;code&gt;crossOrigin="anonymous"&lt;/code&gt;) and tell the server to allow it (&lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;). If you can’t modify the server, relay the image through your backend or use a CORS proxy. Need something production-ready? Give &lt;a href="https://corsfix.com/" rel="noopener noreferrer"&gt;Corsfix&lt;/a&gt; a spin, it’s free to start and only costs when you scale.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>cors</category>
    </item>
  </channel>
</rss>
