Photo: Unsplash
I Compressed 10 Years of Bookmarks Into One Searchable AI Brain
I had 11,400 bookmarks. Safari, Chrome, an old Firefox profile, a Pocket export from before Pocket shut down, and an Instapaper account I’d forgotten the password to. Ten years of “I’ll need this later” — and functionally zero ability to find anything. The folders were archaeology: a “Read later” inside “2017” inside “Old imports”. When I actually needed that article about salary negotiation I saved around 2019, I spent twenty minutes failing to find it and gave up.
One weekend on a Mac Studio fixed it. The result is a local, searchable archive where I type “the article about negotiating by naming a range first” and get the right page in two seconds — found by meaning, not by remembering which folder past-me invented. Here’s the whole pipeline.
Step 1: Get everything out
Every source exports to HTML or CSV:
- Safari: File → Export → Bookmarks →
Safari Bookmarks.html - Chrome: Bookmark Manager (⌥⌘B) → ⋮ → Export bookmarks
- Firefox: Bookmarks → Manage → Import and Backup → Export to HTML
- Pocket/Instapaper: both offered CSV/HTML exports (if you grabbed your Pocket export before the 2025 shutdown, it’s a CSV of URLs and titles)
The bookmark HTML format is ancient Netscape-era markup, which is good news — it’s trivial to parse. A few lines of Python with BeautifulSoup flattens everything into one list:
from bs4 import BeautifulSoup
import glob, csv
links = []
for f in glob.glob("exports/*.html"):
soup = BeautifulSoup(open(f, encoding="utf-8"), "html.parser")
for a in soup.find_all("a"):
links.append((a.get("href"), a.text.strip(),
a.get("add_date", "")))
# dedupe by URL
links = list({u: (u, t, d) for u, t, d in links if u}.values())
print(len(links))
with open("all_links.csv", "w") as out:
csv.writer(out).writerows(links)
After deduplication my 11,400 became 9,800 unique URLs. Already a lesson: I’d re-bookmarked the same pages across browsers for years.
Step 2: The dead-link audit (brace yourself)
Before archiving anything, find out what still exists. This script checks every URL with reasonable politeness:
import csv, concurrent.futures, requests
def check(url):
try:
r = requests.head(url, timeout=10, allow_redirects=True,
headers={"User-Agent": "Mozilla/5.0"})
if r.status_code >= 400: # some servers reject HEAD
r = requests.get(url, timeout=10, stream=True,
headers={"User-Agent": "Mozilla/5.0"})
return url, r.status_code
except Exception:
return url, 0
urls = [row[0] for row in csv.reader(open("all_links.csv"))]
with concurrent.futures.ThreadPoolExecutor(20) as ex:
results = list(ex.map(check, urls))
dead = [u for u, s in results if s == 0 or s >= 400]
print(f"{len(dead)}/{len(urls)} dead")
My result: 3,420 of 9,800 dead — 35%. That’s right in line with link-rot research (Harvard’s studies found ~38% of links from a decade ago broken, and Pew measured 38% of 2013-era pages gone by 2023). A third of what I “saved” over ten years no longer exists. Some came back via the Wayback Machine (https://web.archive.org/web/2019/<url>), but the lesson burned in: a bookmark is a pointer, not a copy, and pointers rot. Everything from here on fixes that.
Step 3: Archive the survivors as single files
For the ~6,400 live links, I fetched actual content. The tool that makes this painless is monolith — it saves a page as one self-contained HTML file with images and CSS inlined:
brew install monolith
monolith "https://example.com/article" -o archive/example-article.html
Wrapped in a loop with a 2-second sleep and a retry pass, the full crawl ran overnight (about nine hours) and produced 14 GB of HTML. For the embedding step you also want clean text, so I ran each file through trafilatura, which strips navigation and ads and outputs readable plaintext:
pip install trafilatura
trafilatura -i filelist.txt --output-dir text/ --no-comments
Roughly 800 pages were paywalls, login walls, or JavaScript husks with no extractable text — accepted losses. Final corpus: ~5,600 readable articles, 310 MB of plaintext. Ten years of reading interests, finally in one folder.
Step 4: The AI brain — embeddings and semantic search
This is where it becomes more than a backup. The easy path: AnythingLLM (free, native Mac app). Create a workspace, point it at the text/ folder, pick a local embedding model, and let it index — about 40 minutes for my corpus on an M2 Ultra, entirely offline. Then you just ask: “that article about negotiation where you should name a range, not a number” — and it returned the 2019 piece I’d failed to find manually, plus two related ones I’d forgotten I saved.
If you’d rather own the pipeline, it’s ~30 lines with a local embedding model:
import ollama, glob, numpy as np
docs, vecs = [], []
for f in glob.glob("text/*.txt"):
body = open(f).read()[:8000]
e = ollama.embed(model="nomic-embed-text", input=body)
docs.append(f); vecs.append(e["embeddings"][0])
V = np.array(vecs)
np.save("vecs.npy", V)
def search(q, k=5):
qv = np.array(ollama.embed(model="nomic-embed-text",
input=q)["embeddings"][0])
sims = V @ qv / (np.linalg.norm(V, axis=1) * np.linalg.norm(qv))
return [docs[i] for i in sims.argsort()[::-1][:k]]
nomic-embed-text via Ollama embeds the whole corpus in under 20 minutes and handles my mixed Czech/English archive surprisingly well — I can query in Czech and find English articles, because good embedding models map both languages into the same meaning-space. For a decade of bilingual bookmarking, that’s the killer feature: no cloud translation API ever sees what I read.
The insight: stop organizing, start indexing
Here’s what the project actually taught me. Those ten years of folder taxonomies — “Dev/JS/Performance”, “Career”, “Recipes maybe” — contributed nothing to the final system. Semantic search ignores them and wins anyway, because folders encode what a link was about to you on the day you saved it, while embeddings encode what it’s about, period. The article I needed was filed under “Work stuff 2019”. Meaning-search found it from the word “negotiation,” which appears nowhere in my folder names.
So the organizational advice is liberating: stop curating, start capturing. Every minute spent deciding which folder a bookmark belongs in is wasted; every page whose content you archive is searchable forever, by any future question, in any language you think in.
Maintenance: the monthly habit that doesn’t rot
The system stays alive with one small loop. New saves go to a single flat inbox.txt of URLs — a Shortcut on Mac and iPhone appends the current tab with one tap, zero filing decisions. Then a monthly cron-style ritual (first Sunday, ~10 minutes of attention):
monolith-batch inbox.txt archive/ # fetch full pages
trafilatura -i new.txt --output-dir text/
python embed_new.py # append to the index
> inbox.txt
Because pages are archived within weeks of saving instead of years, the 35% rot rate drops to near zero — I capture content while it’s alive. The Mac Studio does the work; I just keep the habit.
Total cost of the whole project: zero software dollars, one weekend, and 15 GB of disk. What I got back is the thing bookmarks always promised and never delivered — a memory that actually remembers. If you’ve got a decade of “I’ll need this later” rotting in browser menus, this is your sign: a third of it is already gone, and the clock is running on the rest.
One email a month: the upcoming live event + free recording access for subscribers. No spam, unsubscribe anytime.
