Have you ever found an interesting deal on AppSumo, copied the link somewhere, and then completely forgot about it? This small Chrome extension solves that problem. It lets you search AppSumo directly, save deals (name + link) into local storage, and later edit, delete, export, or import them.
The best part: it’s offline and requires no Google Web Store publishing. You load it locally, and it just works.
⚡ Note on privacy: this extension runs entirely from your local Chrome installation. All your saved data is stored in chrome.storage.local (a sandboxed storage area inside your browser). Nothing is ever uploaded to an external server. This means you keep full control of your data, unlike many extensions that sync or send analytics to third parties.
In this tutorial, I’ll show you step by step how to build it with HTML, CSS, and JavaScript using Manifest V3.
Table of Contents
Step 1 – Set up the project folder
Create a new folder, e.g. appsumo-wishlist-ext/. Inside it, you’ll have four core files:
-
manifest.json -
popup.html -
popup.css -
popup.js
Optionally, you can also include icons (icon16.png, icon48.png, icon128.png).
Step 2 – The manifest.json
Every Chrome extension starts with a manifest. Ours is simple:
{ "manifest_version": 3, "name": "AppSumo Wishlist (Local)", "version": "1.0.0", "description": "Search AppSumo and save deals locally (offline).", "action": { "default_popup": "popup.html", "default_title": "AppSumo Wishlist" }, "permissions": ["storage"], "icons": { "16": "icon16.png", "48": "icon48.png", "128": "icon128.png" } }
We only need the storage permission (to save data locally).
Step 3 – The popup.html
This is the small UI that appears when you click the extension icon in Chrome.
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>AppSumo Wishlist</title> <link rel="stylesheet" href="popup.css" /> </head> <body> <header> <h1>AppSumo Wishlist</h1> </header> <main> <form id="addForm" autocomplete="off"> <label>Software name <input id="name" type="text" placeholder="NeuronWriter" required /> </label> <label>AppSumo URL (optional) <input id="link" type="url" placeholder="https://appsumo.com/products/..." /> </label> <div class="buttons"> <a id="search" class="btn" href="https://appsumo.com/search/?query=" target="_blank" rel="noopener">Search AppSumo</a> <button type="submit" class="primary">Save</button> </div> </form> <section id="list"></section> <section class="tools"> <div class="row"> <button id="export">Export</button> <button id="clear" class="danger">Clear All</button> </div> <div class="paste"> <label for="pasteJson">Paste JSON to import</label> <textarea id="pasteJson" placeholder='Paste exported JSON here'></textarea> <div class="row"> <button id="importPaste">Import Pasted JSON</button> </div> </div> <div id="status" class="status" aria-live="polite"></div> </section> </main> <script src="popup.js" defer></script> </body> </html>
Notice the Search AppSumo link. This is just an <a> tag that dynamically updates based on your input.
Step 4 – The popup.css
A bit of styling to make it look clean and modern.
:root{--bg:#0f172a;--txt:#e5e7eb;--muted:#94a3b8;--ring:#1f2937;--accent:#22c55e;--danger:#ef4444} body{margin:0;background:linear-gradient(180deg,var(--bg),#050a16);color:var(--txt);font:14px system-ui} header{padding:10px 12px;border-bottom:1px solid #111827} h1{margin:0;font-size:15px} main{width:360px;max-width:360px;padding:12px} label{display:block;font-size:12px;color:var(--muted);margin:8px 0 4px} input,textarea{width:100%;background:#0b1220;border:1px solid var(--ring);color:var(--txt);border-radius:8px;padding:8px} .buttons{display:flex;gap:8px;justify-content:flex-end;margin:8px 0} button,.btn{border:1px solid #1f2937;background:#0d1320;color:var(--txt);padding:8px 10px;border-radius:8px;cursor:pointer;text-decoration:none} button.primary{background:linear-gradient(180deg,#22c55e,#16a34a);border-color:#14532d} button.danger{background:linear-gradient(180deg,#ef4444,#b91c1c);border-color:#7f1d1d} .listItem{background:#0c1322;border:1px solid #111827;border-radius:12px;padding:8px;margin:8px 0} .status{margin-top:8px;color:#94a3b8;font-size:12px;min-height:18px}
Step 5 – The popup.js
Here’s where the logic lives: saving, editing, deleting, exporting, and importing.
const $ = s => document.querySelector(s); const KEY = 'appsumo_wishlist_v1'; const fmt = ts => new Date(ts).toLocaleString(); function setStatus(msg, ok = true) { const el = $('#status'); el.textContent = msg || ''; el.style.color = ok ? '#a7f3d0' : '#fca5a5'; if (msg) setTimeout(() => { el.textContent = ''; }, 2500); } function getItems(){ return new Promise(r=> chrome.storage.local.get([KEY], o=> r(o[KEY]||[]))); } function setItems(items){ return new Promise(r=> chrome.storage.local.set({[KEY]:items}, r)); } function escapeHtml(s=''){ return s.replace(/[&<>"']/g, c=>({"&":"&","<":"<",">":">","\"":""","'":"'"})); } function buildSearchUrl(term){ return `https://appsumo.com/search/?query=${encodeURIComponent(term||'')}`; } function render(items){ const list = $('#list'); list.innerHTML = ''; if(!items.length){ list.innerHTML = '<p style="color:#94a3b8">No items yet.</p>'; return; } items.forEach((it,i)=>{ const el=document.createElement('div'); el.className='listItem'; el.innerHTML=`<div class="row"> <div> <div class="name">${escapeHtml(it.name)}</div> <div class="url">${it.link?`<a href="${escapeHtml(it.link)}" target="_blank">${escapeHtml(it.link)}</a>`:'<span style="color:#94a3b8">(no link yet)</span>'}</div> <div class="badge">${fmt(it.ts)}</div> </div> <div class="actions"> <button data-act="open" data-i="${i}">Open</button> <button data-act="copy" data-i="${i}">Copy</button> <button data-act="edit" data-i="${i}">Edit</button> <button data-act="del" data-i="${i}" class="danger">Del</button> </div> </div>`; list.appendChild(el); }); } function updateSearchHref(){ $('#search').setAttribute('href', buildSearchUrl($('#name').value)); } function init(){ getItems().then(render); $('#name').addEventListener('input', updateSearchHref); $('#search').addEventListener('click', updateSearchHref); updateSearchHref(); $('#addForm').addEventListener('submit', async e=>{ e.preventDefault(); const name=$('#name').value.trim(); const link=$('#link').value.trim(); if(!name){ setStatus('Enter a name', false); return; } if(link && !/^https?:\\/\\//i.test(link)){ setStatus('Invalid link', false); return; } const items=await getItems(); items.unshift({name,link,ts:Date.now()}); await setItems(items); render(items); e.target.reset(); updateSearchHref(); setStatus('Saved ✔'); }); $('#list').addEventListener('click', async e=>{ const b=e.target.closest('button'); if(!b) return; const i=+b.dataset.i; const act=b.dataset.act; const items=await getItems(); const it=items[i]; if(!it) return; if(act==='open'){ if(it.link) window.open(it.link,'_blank'); } if(act==='copy'){ await navigator.clipboard.writeText(it.link||it.name); b.textContent='Copied'; setTimeout(()=>b.textContent='Copy',900); } if(act==='edit'){ const newName=prompt('Edit name',it.name)||it.name; const newLink=prompt('Edit link',it.link)||it.link; items[i]={...it,name:newName,link:newLink}; await setItems(items); render(items); } if(act==='del'){ if(confirm('Delete?')){ items.splice(i,1); await setItems(items); render(items);} } }); $('#export').addEventListener('click', async ()=>{ const items=await getItems(); const blob=new Blob([JSON.stringify(items,null,2)],{type:'application/json'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download='appsumo-wishlist.json'; a.click(); URL.revokeObjectURL(url); }); $('#importPaste').addEventListener('click', async ()=>{ try{ const text=$('#pasteJson').value.trim(); if(!text) throw new Error('Nothing to import'); const arr=JSON.parse(text); if(!Array.isArray(arr)) throw new Error('Expected array'); const cleaned=arr.map(x=>({name:x.name||'',link:x.link||'',ts:x.ts||Date.now()})).filter(x=>x.name); if(!cleaned.length) throw new Error('No valid items'); await setItems(cleaned); render(cleaned); $('#pasteJson').value=''; setStatus(`Imported ${cleaned.length} items ✔`); }catch(err){ setStatus('Import failed: '+err.message,false); } }); $('#clear').addEventListener('click', async ()=>{ if(confirm('Clear all?')){ await setItems([]); render([]); } }); } document.addEventListener('DOMContentLoaded', init);
Step 6 – Load the extension in Chrome
-
Open
chrome://extensions -
Enable Developer mode (toggle in top right)
-
Click Load unpacked
-
Select your
appsumo-wishlist-ext/ folder -
Pin the extension (puzzle icon → pin)
Step 7 – Use it!
-
Type a software name, click Search AppSumo → a new browser tab opens with results.
-
Copy the page link for the software you want to add to your wish list, then paste the deal link in the input box and hit Save.
-
Manage your list: Open, Copy, Edit, Delete.
-
Export to JSON or Import by pasting JSON.
Step 8 – Extend it further
You now have a fully working wishlist manager.
Extensions you could add later:
-
“Save Current Tab” button
-
Tags / notes
-
Right-click context menu integration
Conclusion
This little project shows how easy it is to build a Chrome extension with local storage using nothing but HTML, CSS, and JavaScript. It’s a practical tool for keeping track of AppSumo deals — but the pattern can be reused for any kind of wishlist or link manager.
And because it runs locally, your wishlist stays private: no external APIs, no hidden tracking, no data leaks. It’s yours and only yours.
👉 You can download the ready-to-use extension [click the button below].
Digital Designer, blog writer and also a tech enthusiast.
He loves to write content for blogs, podcasts, design websites, logos, brochures, banners and anything that sends a message to an audience.
You can contact Daniele at the link below:



