How to Build a “Read with AI” Sidebar for Any Blog Article
A reader highlights a paragraph about idempotency keys and wants one question answered — "does this apply if my handler writes to two databases?" — without leaving the article, opening a new tab, and re-explaining the paragraph to a general-purpose chatbot. A "Read with AI" sidebar answers that exact question in place: select text, ask, get a grounded answer that cites the section it came from. The interesting engineering problem is not calling an LLM API — it's deciding what context the model actually sees, because sending the whole rendered page on every keystroke is slow, expensive, and produces vaguer answers than sending the one section the reader is actually looking at.
This guide builds one concrete feature: a sidebar on a Next.js blog article that opens on text selection, sends only the selected passage plus its parent section as context, and streams the model's response token-by-token instead of showing a spinner. You'll wire the selection listener, the streaming API route, the token-budget failure that appears when context scoping is skipped, and the decision between a selection sidebar, inline highlight annotations, and a full-page chatbot. The running example is a single article page with a Read with AI button; the same contract — selection, section id, streamed answer — carries through prose, code, and the practice at the end.
Three ways to put AI next to your content
A selection sidebar, inline highlight annotations, and a full-page chatbot all answer "help we understand this content," but they scope context differently and cost differently per question. A selection sidebar triggers on a text highlight and sends only that highlight plus its enclosing section — small, cheap, and precise, but it only answers questions about text the reader already selected. Inline annotations (margin notes generated ahead of time for known-tricky passages) cost nothing per reader because they're precomputed once and served as static content, but they can't answer a question nobody anticipated. A full-page chatbot can answer anything about the whole article, at the cost of sending much more context per turn and a UI that competes with the article for attention.
For a blog where the goal is "clarify the paragraph I'm stuck on," the selection sidebar is the right default: it matches the reader's actual mental state (I don't understand this, not the whole page) and keeps the context small enough to answer fast and cheaply. The build below targets that pattern, with a callout on how to extend it toward the other two.
Quick reference
- Selection sidebar: reader-initiated, context = selection + section, cheapest and most precise per question.
- Inline annotations: author-initiated, precomputed once, zero marginal cost, can't cover unanticipated questions.
- Full-page chatbot: broadest coverage, largest context per turn, competes visually with the article.
- Pick based on the reader's actual intent — "explain this bit" wants a sidebar, not a chatbot tab.
Remember this
A selection sidebar answers "explain this passage" cheaply because its context is scoped to what the reader selected — a full-page chatbot answers a broader class of questions but pays for that breadth on every turn.
Selection capture, section scoping, and the streaming route
The request path has four hops. The reader selects text inside a section with id="atomic-acquire" (any existing article section anchor works — see Preventing cache stampede with Redis locks for a real section built this way). A selectionchange listener on the client captures the selected string and walks up the DOM to find the nearest ancestor with a data-section-id, giving you a section id, not just raw text — that id is what lets the API route pull the actual section body server-side instead of trusting whatever the client sends.
The client posts { sectionId, selectedText, question } to an API route. The route looks up the section's canonical body from the same BlogPost data the page rendered from — never the client's copy of the DOM — builds a prompt of (section body, selected text, question), and calls the model with streaming enabled. The response streams back over the same HTTP connection as Server-Sent Events, and the sidebar renders tokens as they arrive rather than waiting for the full answer.
Quick reference
- Capture
sectionIdfrom the DOM, not raw text — it lets the server fetch a trusted, canonical copy of the section instead of trusting client-submitted context. - Never let the client dictate what context is "the whole page" — the server decides the context window from the section id, closing an obvious prompt-injection vector (see prompt injection in RAG and tool-using agents).
- Stream via SSE (
text/event-stream) or aReadableStreamresponse — perceived latency drops even though total completion time is unchanged. - Debounce the selection listener (150–250ms) so a reader dragging a selection doesn't fire the sidebar prompt on every intermediate mouseup.
Remember this
The server, not the client, decides what context accompanies a question — deriving it from a trusted sectionId lookup instead of client-submitted text closes both a cost problem and a prompt-injection surface.
Building the sidebar: selection listener and streaming fetch
The client side needs two pieces: a listener that turns a browser selection into { sectionId, selectedText }, and a fetch that reads a streamed response body incrementally instead of awaiting response.json(). The API route needs to reject any sectionId it doesn't recognize and cap the section body length before it ever reaches the model call.
Quick reference
- Look up the section server-side from
getBlogPost(slug)— the request only supplies an id, never the trusted content. - Cap the section body length explicitly (
slice(0, 4000)) so one abnormally long section can't blow the token budget silently. - Reject unknown
sectionIdvalues with 400 before any model call — an invalid id is a client bug or a probe, not a question to answer. - Stream the model response through the same
Responseobject rather than buffering — the route returns as soon as the first token exists.
Remember this
The API route is the trust boundary: it re-derives context from a canonical lookup and caps its size, so a modified client request can send a bad sectionId but can never smuggle in arbitrary extra context.
Failure: sending the whole page blows the token budget
The naive first version of this feature often skips section scoping entirely and sends the full rendered article — every section's body concatenated — as context on every question, on the theory that "more context can only help." On a 2,000-word article this adds thousands of prompt tokens per question, most of them irrelevant to what the reader selected. Cost scales with article length instead of question complexity, latency grows because the model has to attend across a much larger context, and answer quality often gets worse — the model has to figure out which of ten unrelated sections the question is actually about, and sometimes answers the wrong one.
The fix is exactly the section-scoping from the previous step, but the failure is worth reproducing once so the fix isn't cargo-culted: remove the sectionId filter, concatenate every section's body into one context string, and ask a question that only makes sense for one specific section. Watch the answer quality and latency both degrade as article length grows, then restore scoping and confirm both recover.
Quick reference
- Whole-page context cost scales with article length, not question complexity — a 5,000-word article costs the same per question as a 500-word one costs per question about everything.
- Larger irrelevant context measurably increases the chance the model answers about the wrong section — this is a real accuracy regression, not just a cost one.
- The fix is the same section-scoping mechanism from the implementation step — this failure exists specifically to prove why that step matters, not to introduce a new mechanism.
- For a multi-section article, always scope by the section the reader is actually looking at; reserve whole-document context for an explicit "summarize the whole article" action, not the default question path.
Remember this
Context size is not free insurance — sending the whole page on every question costs more and frequently answers worse than scoping context to the one section the reader selected.
When to add annotations or a full chatbot instead
Add precomputed inline annotations alongside the sidebar once you notice the same three or four questions repeating in sidebar logs for a given article — those are worth answering once at publish time instead of re-generating per reader. Add a full-page chatbot only when readers are asking questions that span multiple sections or reference material outside the current article ("how does this compare to the Kafka article?") — a selection sidebar's scoped context can't answer that class of question well, because the answer legitimately needs more than one section.
For most technical blogs, ship the selection sidebar first: it is the cheapest to operate, the easiest to reason about for trust boundaries, and matches the actual reader intent ("explain this") most of the time. Treat annotations and a chatbot as additive features once usage data shows the sidebar's scope is the limiting factor, not the starting point.
Quick reference
- Recurring identical sidebar questions on one section → precompute an inline annotation for it instead of paying for the same generation repeatedly.
- Cross-section or cross-article questions → the sidebar's scoped context is structurally the wrong shape; that's a chatbot's job.
- Ship the sidebar first — it's the cheapest, most trust-boundary-obvious option and matches the common reader intent.
- Log
sectionId+ question pairs (without storing full selected text longer than needed) to see which pattern your readers actually want next.
Remember this
Start with the selection sidebar because it matches the common intent at the lowest cost; add annotations for repeat questions and a chatbot only once real usage shows readers need answers that cross section or article boundaries.
Key takeaway
Build the sidebar against one real article section (sectionId: "atomic-acquire" from prevent-cache-stampede-redis works, or use one from your own page). Select the paragraph explaining the atomic acquire, ask "what happens if two requests call SET at the exact same millisecond?", and confirm the answer streams token-by-token and references the atomic-command mechanism from that section specifically.
Then break it intentionally: remove the sectionId lookup in the API route and concatenate every section's body into the context instead, then ask the same question. Expected regression: slower response, and the answer may drift toward a different section's mechanism (e.g. the release-bug section) instead of staying on the atomic-acquire one. Recovery: restore the section lookup and the 4,000-character cap. Pass criterion: with scoping restored, the same question reliably answers using only the selected section's mechanism, and an unknown sectionId reliably returns 400 without ever reaching the model call.
Related Articles
Explore this topic