<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: tinycoder-studio</title>
    <description>The latest articles on DEV Community by tinycoder-studio (@tinycoder-studio).</description>
    <link>https://dev.to/tinycoder-studio</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4107174%2F850ef756-9a6f-444a-b476-65c612c20f78.png</url>
      <title>DEV Community: tinycoder-studio</title>
      <link>https://dev.to/tinycoder-studio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tinycoder-studio"/>
    <language>en</language>
    <item>
      <title>I Found an Undocumented MCP Server on OpenSea — and It Leaked Usernames for Any Wallet</title>
      <dc:creator>tinycoder-studio</dc:creator>
      <pubDate>Sun, 13 Sep 2026 03:28:36 +0000</pubDate>
      <link>https://dev.to/tinycoder-studio/i-found-an-undocumented-mcp-server-on-opensea-and-it-leaked-usernames-for-any-wallet-43om</link>
      <guid>https://dev.to/tinycoder-studio/i-found-an-undocumented-mcp-server-on-opensea-and-it-leaked-usernames-for-any-wallet-43om</guid>
      <description>&lt;h1&gt;
  
  
  I Found an Undocumented MCP Server on OpenSea — and It Leaked Usernames for Any Wallet
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; OpenSea runs an undocumented MCP server at &lt;code&gt;mcp.opensea.io/mcp&lt;/code&gt;. One of its tools hands out API keys to anyone who asks, and another lets you resolve any Ethereum address &lt;em&gt;with an OpenSea profile&lt;/em&gt; to its owner's username. Reported to Bugcrowd, now sharing the story and a free recon kit.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;I was poking around OpenSea's tooling one afternoon — just trying to understand how some of their internal features worked. Nothing fancy, no scanners, no brute force. Just curiosity and a browser.&lt;/p&gt;

&lt;p&gt;That's when I stumbled onto something that wasn't meant for public eyes: an undocumented MCP server sitting at &lt;code&gt;mcp.opensea.io/mcp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At first glance it looked like just another internal API. But one particular tool caught my attention: &lt;code&gt;get_instant_api_key&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Discovery
&lt;/h2&gt;

&lt;p&gt;Turns out, that little endpoint was an unintended exposure of something far more valuable. By sending a straightforward POST request to &lt;code&gt;https://api.opensea.io/api/v2/auth/keys&lt;/code&gt;, I could pull a valid API key straight out of the server. No authentication hoops — just a direct line to a credential that should have stayed hidden.&lt;/p&gt;

&lt;p&gt;With that key in hand, I called the &lt;code&gt;get_profile&lt;/code&gt; tool and, suddenly, I could look up a username for any Ethereum address with an OpenSea profile. I tested it on my own wallet, then on a well-known one. Both resolved instantly.&lt;/p&gt;

&lt;p&gt;It felt like finding a master key to a building you weren't supposed to enter.&lt;/p&gt;

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

&lt;p&gt;Information disclosure like this isn't just a curiosity — it's a useful starting point for attackers. Enumerating usernames might seem harmless, but it's often the first step in mapping out a target's footprint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tie a wallet to a real identity (doxxing vector)&lt;/li&gt;
&lt;li&gt;Correlate multiple wallets to one person&lt;/li&gt;
&lt;li&gt;Build a profile for targeted phishing or social engineering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For NFT holders, that's a real privacy concern. Your wallet is supposed to be pseudonymous — this tool weakens that pseudonymity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What OpenSea Did Right
&lt;/h2&gt;

&lt;p&gt;To be fair, not everything on that server was exposed. The sensitive tools — like &lt;code&gt;cancel_orders&lt;/code&gt; — properly required a wallet JWT with &lt;code&gt;write:orders&lt;/code&gt; scope. Trying to call them without it returned a clean &lt;code&gt;MISSING_SCOPE&lt;/code&gt; error.&lt;/p&gt;

&lt;p&gt;So the architecture is sound. The gap was in the &lt;em&gt;discovery&lt;/em&gt; layer: a handful of tools were reachable with just an API key that anyone could mint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Responsible Disclosure
&lt;/h2&gt;

&lt;p&gt;I reported this to OpenSea's Bugcrowd program with a full write-up and PoC. It's now in their triage queue (self-assessed as P4 informational — username enumeration; official severity pending triage).&lt;/p&gt;

&lt;p&gt;I have no affiliation with OpenSea or Bugcrowd.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're a security researcher:&lt;/strong&gt; don't go poking at this yourself. It's been reported, and testing live systems without authorization is both against program rules and a great way to get banned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Free Recon Kit
&lt;/h2&gt;

&lt;p&gt;I turned the whole investigation into a free PDF — &lt;strong&gt;the OpenSea MCP Server Recon Kit&lt;/strong&gt; — with the methodology, the tools I enumerated, and the lessons learned. It's pay-what-you-want (free, $0 minimum), and it's the same playbook I use when auditing any undocumented API surface. The kit contains methodology only — no exploit code:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://tinycoderstudio.gumroad.com/l/opensea-mcp-recon-kit" rel="noopener noreferrer"&gt;Download the free recon kit&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Lesson
&lt;/h2&gt;

&lt;p&gt;Undocumented internal APIs are everywhere. They're often overlooked during security reviews, yet they can leak exactly the kind of secrets — like API keys — that turn a minor misconfiguration into a real problem.&lt;/p&gt;

&lt;p&gt;The next time you're auditing a target, ask yourself: &lt;em&gt;what endpoints exist that nobody documented?&lt;/em&gt; That's where the interesting stuff hides.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This technique was reported to Bugcrowd and is shared here for educational purposes only.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Found this useful? Follow me for more bug bounty write-ups and recon methodology.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>bugbounty</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Found an Undocumented MCP Server on OpenSea — and It Leaked Usernames for Any Wallet</title>
      <dc:creator>tinycoder-studio</dc:creator>
      <pubDate>Sat, 12 Sep 2026 23:52:09 +0000</pubDate>
      <link>https://dev.to/tinycoder-studio/i-found-an-undocumented-mcp-server-on-opensea-and-it-leaked-usernames-for-any-wallet-5935</link>
      <guid>https://dev.to/tinycoder-studio/i-found-an-undocumented-mcp-server-on-opensea-and-it-leaked-usernames-for-any-wallet-5935</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; OpenSea runs an undocumented MCP server at &lt;code&gt;mcp.opensea.io/mcp&lt;/code&gt;. One of its tools hands out API keys to anyone who asks, and another lets you resolve any Ethereum address &lt;em&gt;with an OpenSea profile&lt;/em&gt; to its owner's username. Reported to Bugcrowd, now sharing the story and a free recon kit.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;I was poking around OpenSea's tooling one afternoon — just trying to understand how some of their internal features worked. Nothing fancy, no scanners, no brute force. Just curiosity and a browser.&lt;/p&gt;

&lt;p&gt;That's when I stumbled onto something that wasn't meant for public eyes: an undocumented MCP server sitting at &lt;code&gt;mcp.opensea.io/mcp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At first glance it looked like just another internal API. But one particular tool caught my attention: &lt;code&gt;get_instant_api_key&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Discovery
&lt;/h2&gt;

&lt;p&gt;Turns out, that little endpoint was an unintended exposure of something far more valuable. By sending a straightforward POST request to &lt;code&gt;https://api.opensea.io/api/v2/auth/keys&lt;/code&gt;, I could pull a valid API key straight out of the server. No authentication hoops — just a direct line to a credential that should have stayed hidden.&lt;/p&gt;

&lt;p&gt;With that key in hand, I called the &lt;code&gt;get_profile&lt;/code&gt; tool and, suddenly, I could look up a username for any Ethereum address with an OpenSea profile. I tested it on my own wallet, then on a well-known one. Both resolved instantly.&lt;/p&gt;

&lt;p&gt;It felt like finding a master key to a building you weren't supposed to enter.&lt;/p&gt;

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

&lt;p&gt;Information disclosure like this isn't just a curiosity — it's a goldmine for attackers. Enumerating usernames might seem harmless, but it's often the first step in mapping out a target's footprint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tie a wallet to a real identity (doxxing vector)&lt;/li&gt;
&lt;li&gt;Correlate multiple wallets to one person&lt;/li&gt;
&lt;li&gt;Build a profile for targeted phishing or social engineering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For NFT holders, that's a real privacy concern. Your wallet is supposed to be pseudonymous — this tool quietly erases that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What OpenSea Did Right
&lt;/h2&gt;

&lt;p&gt;To be fair, not everything on that server was exposed. The sensitive tools — like &lt;code&gt;cancel_orders&lt;/code&gt; — properly required a wallet JWT with &lt;code&gt;write:orders&lt;/code&gt; scope. Trying to call them without it returned a clean &lt;code&gt;MISSING_SCOPE&lt;/code&gt; error.&lt;/p&gt;

&lt;p&gt;So the architecture is sound. The gap was in the &lt;em&gt;discovery&lt;/em&gt; layer: a handful of tools were reachable with just an API key that anyone could mint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Responsible Disclosure
&lt;/h2&gt;

&lt;p&gt;I reported this to OpenSea's Bugcrowd program with a full write-up and PoC. It's now in their triage queue (self-assessed as P4 informational — username enumeration; official severity pending triage).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're a security researcher:&lt;/strong&gt; don't go poking at this yourself. It's been reported, and testing live systems without authorization is both against program rules and a great way to get banned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Free Recon Kit
&lt;/h2&gt;

&lt;p&gt;I turned the whole investigation into a free PDF — &lt;strong&gt;the OpenSea MCP Server Recon Kit&lt;/strong&gt; — with the methodology, the tools I enumerated, and the lessons learned. It's pay-what-you-want (free, $0 minimum), and it's the same playbook I use when auditing any undocumented API surface. The kit contains methodology only — no exploit code:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://tinycoderstudio.gumroad.com/l/opensea-mcp-recon-kit" rel="noopener noreferrer"&gt;Download the free recon kit&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Lesson
&lt;/h2&gt;

&lt;p&gt;Undocumented internal APIs are everywhere. They're often overlooked during security reviews, yet they can leak exactly the kind of secrets — like API keys — that turn a minor misconfiguration into a real problem.&lt;/p&gt;

&lt;p&gt;The next time you're auditing a target, ask yourself: &lt;em&gt;what endpoints exist that nobody documented?&lt;/em&gt; That's where the interesting stuff hides.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This technique was reported to Bugcrowd and is shared here for educational purposes only.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Found this useful? Follow me for more bug bounty write-ups and recon methodology.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
    </item>
    <item>
      <title>I Rewrote 5 AI-Generated Paragraphs to Sound Human — Here's the Before &amp; After</title>
      <dc:creator>tinycoder-studio</dc:creator>
      <pubDate>Thu, 10 Sep 2026 00:13:37 +0000</pubDate>
      <link>https://dev.to/tinycoder-studio/i-rewrote-5-ai-generated-paragraphs-to-sound-human-heres-the-before-after-3i0p</link>
      <guid>https://dev.to/tinycoder-studio/i-rewrote-5-ai-generated-paragraphs-to-sound-human-heres-the-before-after-3i0p</guid>
      <description>&lt;p&gt;Last week I wrote about building an AI humanizer that actually sounds human. &lt;a class="mentioned-user" href="https://dev.to/topstar_ai"&gt;@topstar_ai&lt;/a&gt; pointed out the hardest challenge — and they're right. The hardest part isn't making AI text longer. It's making it feel like someone actually meant what they wrote.&lt;/p&gt;

&lt;p&gt;Here's what I found.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Uniform rhythm.&lt;/strong&gt; Every sentence is 12-18 words. Humans vary wildly — some thoughts are three words, others spill across two sentences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Hedging and filler.&lt;/strong&gt; "It's important to note that..." "In today's world..." These phrases add zero meaning and scream template.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Empty transitions.&lt;/strong&gt; "Furthermore," "Moreover," "Additionally" — humans say "also" or just skip the transition entirely.&lt;/p&gt;

&lt;p&gt;None of these are wrong. They're just &lt;em&gt;empty&lt;/em&gt;. And once you spot them, you can't unsee them.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;1. Product description (generic SaaS pitch)&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AI:&lt;/em&gt; Our platform provides a comprehensive suite of tools designed to help teams collaborate more effectively and streamline their workflow.&lt;br&gt;
&lt;em&gt;Human:&lt;/em&gt; It helps teams stop chasing each other across Slack threads.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Annotation:&lt;/strong&gt; Cut the feature list. Gave it a problem and a punch.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;2. Blog intro (the classic "In this article, we'll explore...")&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AI:&lt;/em&gt; In this article, we will explore the top trends shaping the future of web development in 2025 and beyond.&lt;br&gt;
&lt;em&gt;Human:&lt;/em&gt; Web dev moved fast in 2024. Here's what actually mattered.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Annotation:&lt;/strong&gt; Killed the meta-commentary. Started mid-thought like a human would.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;3. Social media post (LinkedIn thought-leadership)&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AI:&lt;/em&gt; Furthermore, it is important to recognize that leadership in today's world requires adaptability and a commitment to continuous learning.&lt;br&gt;
&lt;em&gt;Human:&lt;/em&gt; Adapt or die. That's been true forever — but most people still skip the uncomfortable part.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Annotation:&lt;/strong&gt; Removed "furthermore" and "it is important to recognize." Added an opinion.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;4. Email outreach (cold template)&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AI:&lt;/em&gt; I hope this message finds you well. I am reaching out to introduce our product, which I believe could be of great value to your organization.&lt;br&gt;
&lt;em&gt;Human:&lt;/em&gt; Your site's load time is 4 seconds. Here's how to cut it to 1.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Annotation:&lt;/strong&gt; No greeting, no filler. Led with a specific number they can't ignore.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;5. Error message / technical explanation&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AI:&lt;/em&gt; An unexpected error has occurred. Please try again or contact support if the issue persists.&lt;br&gt;
&lt;em&gt;Human:&lt;/em&gt; Something broke on our end. We're fixing it — refresh in 30 seconds and it should work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Annotation:&lt;/strong&gt; Added a timeframe and ownership. Humans explain what's happening and when it ends.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Human writing has opinions, rhythm changes, and friction. AI writing is smooth, safe, and forgettable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the whole game. If you're tired of generating text that reads like a committee wrote it, check out &lt;strong&gt;PseudoHuman&lt;/strong&gt; at &lt;a href="https://toolbox-lilac-three.vercel.app" rel="noopener noreferrer"&gt;toolbox-lilac-three.vercel.app&lt;/a&gt; — it takes your AI draft and injects the personality it's missing.&lt;/p&gt;

&lt;p&gt;What's the tell-tale sign that makes &lt;em&gt;you&lt;/em&gt; stop reading something that sounds AI-generated?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>javascript</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I'm Building an AI Humanizer That Actually Sounds Human — Here's What I'm Learning</title>
      <dc:creator>tinycoder-studio</dc:creator>
      <pubDate>Wed, 09 Sep 2026 14:22:02 +0000</pubDate>
      <link>https://dev.to/tinycoder-studio/im-building-an-ai-humanizer-that-actually-sounds-human-heres-what-im-learning-38kd</link>
      <guid>https://dev.to/tinycoder-studio/im-building-an-ai-humanizer-that-actually-sounds-human-heres-what-im-learning-38kd</guid>
      <description>&lt;p&gt;Hey — I'm building an AI humanizer in public, and I wanted to share what I'm learning before there's anything to launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I'm seeing
&lt;/h2&gt;

&lt;p&gt;Most "humanizers" I've tried just swap synonyms. You know the output instantly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Utilize" instead of "use." Perfect grammar. Zero rhythm. Every sentence the same length.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It passes a thesaurus check and fails the human check. Real people write messy — fragments, dashes, short punches after long rambles. That's what I'm trying to capture.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm building (honestly, still early)
&lt;/h2&gt;

&lt;p&gt;No beta yet. No launch. I'm building a small tool that rewrites AI-sounding text so it aims to sound like a person wrote it — rhythm, burstiness, tone shifts, not just word swaps.&lt;/p&gt;

&lt;p&gt;Right now I'm just testing in the open and learning from real examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me so far
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rhythm beats vocabulary.&lt;/strong&gt; Varying sentence length does more for "sounds human" than any synonym swap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Imperfection is a feature.&lt;/strong&gt; A fragment. An em dash. A blunt short sentence. Readers trust it more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context matters more than cleverness.&lt;/strong&gt; Same input needs a different rewrite for an essay vs. a tweet vs. an email.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A tiny before/after (from my own testing notes, still rough)
&lt;/h2&gt;

&lt;p&gt;Before (typical AI):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"In today's fast-paced world, it is important to utilize effective tools for content creation."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What I'm aiming for:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Everyone's drowning in AI-sounding content. I'm trying to make mine sound like I actually wrote it."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Still rough. That's the point of building in public.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I want from you
&lt;/h2&gt;

&lt;p&gt;Drop a sentence that &lt;em&gt;screams&lt;/em&gt; AI in the comments — I'll reply with how I'd approach rewriting it, and what I'd still get wrong. Teaching in the open, learning in the open.&lt;/p&gt;

&lt;p&gt;If you want a note when the early access list opens, follow along. No spam, just the build log.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>writing</category>
      <category>buildinpublic</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Vanilla JavaScript PWA Tutorial: Build With Zero Dependencies</title>
      <dc:creator>tinycoder-studio</dc:creator>
      <pubDate>Mon, 07 Sep 2026 10:05:39 +0000</pubDate>
      <link>https://dev.to/tinycoder-studio/vanilla-javascript-pwa-tutorial-build-with-zero-dependencies-1cpj</link>
      <guid>https://dev.to/tinycoder-studio/vanilla-javascript-pwa-tutorial-build-with-zero-dependencies-1cpj</guid>
      <description>&lt;p&gt;Every PWA tutorial starts with "install React, then Next.js, then configure Webpack..." By the time you're done, you have 200MB of node_modules for a to-do app.&lt;/p&gt;

&lt;p&gt;This tutorial uses zero dependencies. Just HTML, CSS, and JavaScript. You'll have a working PWA in 10 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;

&lt;p&gt;A simple web app that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Works offline&lt;/li&gt;
&lt;li&gt;Installs to the home screen&lt;/li&gt;
&lt;li&gt;Has a service worker for caching&lt;/li&gt;
&lt;li&gt;Runs on any static host (Vercel, Netlify, GitHub Pages)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: The HTML
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;index.html&lt;/code&gt;:&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="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;My PWA&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"manifest"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/manifest.json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"theme-color"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"#0F172A"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"apple-touch-icon"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/icon-192.png"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Hello, PWA!&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;This app works offline.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/app.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: The Manifest
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;manifest.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My PWA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"short_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PWA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"display"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"standalone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"background_color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#0F172A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"theme_color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#06B6D4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"icons"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/icon-192.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"192x192"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/icon-512.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"512x512"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: The Service Worker
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;sw.js&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CACHE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-pwa-v1&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;ASSETS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/index.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/app.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/style.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CACHE&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;cache&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cache&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;ASSETS&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="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;activate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&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;keys&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keys&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;k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;CACHE&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;k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;caches&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;k&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="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&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 service worker does three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Install&lt;/strong&gt; — caches all app files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Activate&lt;/strong&gt; — deletes old caches&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fetch&lt;/strong&gt; — serves from cache, falls back to network&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Register the Service Worker
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;app.js&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;serviceWorker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/sw.js&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;reg&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SW registered:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SW failed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Deploy
&lt;/h2&gt;

&lt;p&gt;If you're using Vercel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; vercel
vercel deploy &lt;span class="nt"&gt;--prod&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Your PWA is live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Offline
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open Chrome DevTools → Application → Service Workers&lt;/li&gt;
&lt;li&gt;Check "Offline" in the Network tab&lt;/li&gt;
&lt;li&gt;Refresh the page — it should still load&lt;/li&gt;
&lt;li&gt;On Android: three-dot menu → "Add to Home Screen"&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why Vanilla JS?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero dependencies&lt;/strong&gt; — nothing to update, nothing to break&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast&lt;/strong&gt; — no build step, no transpilation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small&lt;/strong&gt; — the entire app is under 10KB gzipped&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Understandable&lt;/strong&gt; — you can read every line&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;React and Next.js are great for complex apps. But for a simple PWA? Vanilla JS is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Want a head start?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://tinycoderstudio.gumroad.com/l/gyhehh?utm_source=devto&amp;amp;utm_medium=vanilla-pwa&amp;amp;utm_campaign=lead_gen_2026" rel="noopener noreferrer"&gt;TinyCoder&lt;/a&gt; is a vanilla JS PWA with 5 offline dev tools. Config-driven, deployable in minutes.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://toolbox-lilac-three.vercel.app/blog/vanilla-js-pwa-tutorial.html" rel="noopener noreferrer"&gt;toolbox-lilac-three.vercel.app/blog/vanilla-js-pwa-tutorial.html&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
