Someone smart left me a comment months ago: the killer feature isn't the citations, it's page-level citations. NotebookLM hands you a chunk of a PDF, not a page. Map that chunk back to "page 23" while everyone else just says "somewhere in a 40-page pdf," and that's the thing researchers actually pay for. He called it the moat.
He was right that it'd be a moat. He was also, it turns out, describing something I couldn't build. Here's the week I spent finding that out, and why I think finding it out was the actual work.
I started convinced it was a scraping problem. NotebookLM shows you the source when you click a citation, so the page number must be in there somewhere, right? I just had to find the right element. Classic.
First I hovered a citation and a little card popped up with the exact quote from the PDF. No page number on it, but fine — maybe it's in the data behind the card. So I watched the network tab while clicking citations. The request that fired was 140 bytes. A hundred and forty. That's not a page number payload, that's a "user clicked citation 4" analytics ping. Dead end.
Then I thought: maybe the page number is in the big document-load response, just not shown in the UI. So I exported the full network log — 26 separate backend calls — and wrote a script to search every single response for anything that smelled like a page: pageNumber, pageSpan, numPages, boundingBox, all of it. Zero hits. Not one. What I did find, everywhere, was character offsets — "this quote starts at character 14037." That's how NotebookLM locates things internally. It flattened the PDF into one long string of text and threw the page boundaries away. The page number wasn't hidden from me. It doesn't exist on their side at all.
So the only way to get a real page number is to do it myself: get the user's original PDF, run it through pdf.js page by page, and match the quote text to find which page it landed on. Which sounds doable until you hit two walls. One, I'm not sure I can even get the original PDF — the user uploaded it to Google, not to me. Two, even if I could, the quote NotebookLM gives back is reworded, not word-for-word from the source, so my matching could confidently point at the wrong page. And "confidently wrong" is worse than "honestly vague" for the exact researchers I'm trying to win. Nothing kills a precision feature faster than it being imprecise.
That's the moment the honest move was to stop. Not because it's technically impossible forever, but because the cost is high, the payoff is uncertain, and — the part that actually matters — nobody is asking me for it yet. The commenter was giving me a strategy, not a bug report from a user. Building a hard, fragile feature for a demand that hasn't shown up is how solo devs disappear for three months and ship to silence.
So I built the version that was actually reachable. Click a citation, NotebookLM highlights the source passage on the left, and my extension grabs that passage — the quote, the source title, the character offset — and saves it as a citation you can export to APA, MLA, BibTeX, with the original text attached so you can verify it later. Not page-level. Passage-level. It does the thing researchers actually need most day to day — "did the AI make this up, or is it really in the source" — without the part I can't do reliably.
And there was a small, humbling bug in even that. My first version grabbed the hover card. Worked great in my head. Except the hover card vanishes the instant you move the mouse toward my panel to click Import. I'd written code for a flow that physically cannot happen. Only when I actually tried to use it, like a user, did I see it. Switched to grabbing the highlight that stays put after a click. Lesson I keep re-learning: test the gesture, not just the function.
I logged the page-number path in my roadmap with the full autopsy — why it's blocked, what I'd need, when it'd be worth revisiting. If a real researcher ever emails me "I need the page," I've already done the hard thinking and I'll know exactly what to try. Until then it stays a documented no, not a half-built maybe rotting in the codebase.
Is proving a feature can't be built a waste of a week? I don't think so. I now know exactly where the ceiling is, I shipped the useful 80% underneath it, and I didn't burn a month chasing the 20% nobody's asked for. Knowing what not to build might be the most underrated skill in this whole thing.
What's a feature you were sure you wanted, until you actually dug into what it'd cost?
— building NotebookBloom in public, #8
Top comments (22)
Proving a feature is a bad idea is a huge time-saver, especially when it comes to page-level rendering or complex data fetching where the technical debt would outweigh the user value. It reminds me of the classic trap of over-engineering a solution for a niche use case instead of shipping the core MVP. I actually had to pivot our initial architecture for a similar reason when putting together our Next.js and Supabase SaaS starter, realizing that keeping the data layer simple was way more valuable than building a custom page-level caching system. It is exactly why we kept the boilerplate at PubliFlow so lean, focusing on shipping fast rather than adding complex features that might not survive contact with real users. How did your team handle the transition after you killed that feature, and did it free up bandwidth for something that actually moved the needle?
ha, "team" is just me — so the transition was basically me closing 30 browser tabs and exhaling. but honestly the passage-level version was already 90% there while i was chasing pages, so shipping it took a couple days, not a rewrite. the real thing it freed up wasn't hours, it was headspace — i stopped treating "someday page numbers" as a debt hanging over every decision. wrote the full autopsy into the roadmap and let it go. that lean-boilerplate instinct you're describing is exactly it: the discipline isn't building fast, it's not building the thing nobody asked for yet.
Reclaiming headspace over raw hours is the ultimate ROI, especially when you are flying solo and carrying all the cognitive load. Dropping that mental debt probably doubled your actual coding velocity since you no longer have to context-switch out of guilt. Did writing the autopsy help you formalize a checklist for evaluating future features, or was it mostly just for your own closure?
started as closure, honestly. but one line of it stuck and became a rule i actually use now: before i build anything, "can i really get the data, and did anyone ask for this yet?" the page-number thing failed both — the data wasn't in the DOM, and nobody had asked, i just wanted it to exist. it's not a fancy checklist, more a gut-check i can't unsee anymore. that's the sneaky part of writing the autopsy down — you think you're closing a door and you end up with a filter.
That two-part gut check is brilliant because it attacks both technical feasibility and actual user demand before a single line of code is written. Writing things down really does force that clarity, turning a fleeting realization into a permanent mental filter for future work. Do you find yourself applying this same data and demand check to refactoring existing features, or strictly to new builds?
i use it on refactors too, but the second question flips. for a new build it's "did anyone ask for this." for a refactor it's "is anything actually broken, or am i just bored with the code." that one catches me way more often — the itch to rewrite something that works fine because a cleaner version lives in my head. so refactors get a third question stapled on: "will a user ever feel this change." if the answer's no, it's not a refactor, it's me procrastinating with extra steps. honestly the demand check is even more useful there — nobody files a bug asking you to make your code prettier.
That third question about whether a user will actually feel the change is the ultimate reality check for developer-driven refactoring. It is so easy to fall into the trap of rewriting working code just because a more elegant solution exists in our heads. Do you find that framing the refactor around a tangible user benefit helps secure buy-in from product, or do you still have to fight for the engineering time?
no product team to win over here — the fight is mostly with myself. if i can't name what gets faster, safer, or less annoying for the user, the refactor goes behind bugs and requested work. cleaner code can still be the wrong thing to build today.
Disciplining yourself to prioritize user value over engineering sensibilities is a tough mindset shift. It is so easy to fall into the trap of refactoring just to satisfy our own desire for clean code. When you do eventually circle back to those deferred refactors, do you have a specific trigger or metric that pushes them out of the backlog?
no formal metric. it comes back when it starts charging interest: the same bug twice, the same workaround in a third feature, or it blocks something a user actually asked for. until then, the backlog note stays a note.
The concept of ignored friction 'charging interest' through recurring bugs and duplicated workarounds perfectly captures the hidden cost of deferring those issues. It makes me wonder if tracking these specific interest payments could actually help quantify the pain when grooming the backlog with product managers. Have you found a good way to surface these hidden costs before they eventually block a major release?
not a clean system yet. the closest thing i have is adding a receipt to the original roadmap note every time i pay the tax: another bug, workaround, or extra touch. once the same item has three receipts, it stops being “code i'd like cleaner” and becomes scheduled work.
That receipt system is a clever way to make invisible debt visible. Setting a hard threshold of three receipts before scheduling the refactor prevents the work from slipping through the cracks while keeping the backlog from getting bloated. Have you found that developers actually remember to log those receipts in the heat of the moment, or does it require a strict process change to stick?
the developer is still just me, so there's no process to enforce. i add one line to the same roadmap note before i fix or work around the issue. if logging it took longer than twenty seconds, i'd stop doing it. the system sticks because the receipt is cheaper than forgetting.
That twenty-second friction limit is a brilliant constraint for a solo workflow. When the overhead of tracking debt exceeds the mental load of just carrying it, the system naturally dies. Keeping it as a single roadmap line perfectly balances visibility with velocity, proving that the best processes are the ones that feel like no process at all. Have you found that reviewing this single note eventually reveals patterns in where the debt is actually accumulating?
yeah — the receipts cluster at boundaries i don't own: NotebookLM's DOM, YouTube's JSON, auth and billing handoffs. internal code rarely earns three receipts; adapters do. that's useful because it tells me where to isolate disposable code instead of “cleaning up” the whole app.
Framing third-party adapters as intentionally disposable code completely changes how you approach over-engineering. Instead of building bulletproof abstractions for external APIs, you just let them be messy and isolated. Do you version those disposable adapters separately from your core domain logic when the external API inevitably changes?
not separately versioned. they live behind narrow interfaces in their own files, but ship with the same extension build. a second version number would add ceremony without buying compatibility — there's still only one deployed bundle. i care about containing the blast radius, not creating another package.
That makes a lot of sense; relying on narrow interfaces for logical isolation avoids the overhead of managing multiple package versions for a single deployed bundle. Containing the blast radius is definitely the priority over bureaucratic versioning. Do you use any specific linting rules or architectural fitness functions to enforce those narrow boundaries during CI?
not yet. right now it's enforced by the adapter interface, file boundaries, and TypeScript—not an architectural CI check. the build catches contract drift, but not someone reaching around the adapter. an import-boundary rule is probably the first guardrail i'd add as the codebase grows.
Relying on TypeScript and file boundaries is a solid starting point, but you are absolutely right that it leaves the door open for accidental bypasses. Adding an import-boundary rule via ESLint or a tool like dependency-cruiser early on can save you from untangling a massive dependency graph later. Have you considered using dependency-cruiser to visualize and block those forbidden imports before they even hit the CI pipeline?
not yet. the adapter graph is still small enough to inspect directly, and there's no CI pipeline to plug it into. my trigger would be a second consumer or another contributor; then i'd start with one forbidden-import rule, not a full architecture dashboard.