<?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: Sophia Castillo</title>
    <description>The latest articles on DEV Community by Sophia Castillo (@sphcastillo).</description>
    <link>https://dev.to/sphcastillo</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%2F4133179%2F16ac3337-ff85-4130-ae97-7c3d53912aa1.jpg</url>
      <title>DEV Community: Sophia Castillo</title>
      <link>https://dev.to/sphcastillo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sphcastillo"/>
    <language>en</language>
    <item>
      <title>Everlogue: I Built a Book Catalog That Refuses to Trust Its Own Automation</title>
      <dc:creator>Sophia Castillo</dc:creator>
      <pubDate>Mon, 05 Oct 2026 00:26:16 +0000</pubDate>
      <link>https://dev.to/sphcastillo/everlogue-i-built-a-book-catalog-that-refuses-to-trust-its-own-automation-3i7a</link>
      <guid>https://dev.to/sphcastillo/everlogue-i-built-a-book-catalog-that-refuses-to-trust-its-own-automation-3i7a</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/sanity-2026-09-16"&gt;Sanity Challenge, Path Two: Vibe-Code Something Strange&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built Everlogue, a home for everything you read: a shared book catalog, private reading shelves, celebrity book clubs, and Ask Everlogue, a reading companion grounded in structured Sanity content.&lt;/p&gt;

&lt;p&gt;But I did not want to build another Goodreads clone with a nicer frontend.&lt;/p&gt;

&lt;p&gt;I wanted to answer a harder question:&lt;/p&gt;

&lt;p&gt;What if the catalog could grow itself, while Sanity decided what was trusted enough to become shared content?&lt;/p&gt;

&lt;p&gt;That led me to build Everlogue around multiple ingestion paths, with Sanity as the editorial control system.&lt;/p&gt;

&lt;p&gt;Bulk / historical ingestion&lt;/p&gt;

&lt;p&gt;Readers can import their Goodreads library. Everlogue parses the CSV, works through the books in the reader's library, matches or enriches metadata, and places books onto the correct Want to Read, Currently Reading, and Read shelves.&lt;/p&gt;

&lt;p&gt;This gives a reader a way to bring an existing reading history into Everlogue instead of rebuilding it book by book.&lt;/p&gt;

&lt;p&gt;Reader-grown catalog&lt;/p&gt;

&lt;p&gt;Readers can also grow the shared catalog.&lt;/p&gt;

&lt;p&gt;If someone searches for a book that Everlogue does not have and tries to add it to a shelf, that missing title becomes a catalog request for review.&lt;/p&gt;

&lt;p&gt;Instead of every reader creating their own disconnected version of the same book, Everlogue can review the request and turn it into structured shared catalog content.&lt;/p&gt;

&lt;p&gt;Continuous / live ingestion: Book Club Watch&lt;/p&gt;

&lt;p&gt;The strangest ingestion path became Book Club Watch.&lt;/p&gt;

&lt;p&gt;Everlogue follows Reese's Book Club, GMA Book Club, Read With Jenna, and Oprah's Book Club. Keeping those collections current sounds simple until you realize they do not announce their selections on the same schedule.&lt;/p&gt;

&lt;p&gt;I modeled each club separately:&lt;/p&gt;

&lt;p&gt;Reese&lt;br&gt;
9 AM ET, days 1–8&lt;br&gt;
Stop once the month's pick is already recorded&lt;/p&gt;

&lt;p&gt;GMA&lt;br&gt;
10 AM ET every Tuesday&lt;/p&gt;

&lt;p&gt;Read With Jenna&lt;br&gt;
9 AM ET Monday + Tuesday&lt;br&gt;
Only during the early-month window&lt;/p&gt;

&lt;p&gt;Oprah&lt;br&gt;
9 AM ET Monday / Wednesday / Friday&lt;br&gt;
Recurring checks because there is no fixed release pattern&lt;/p&gt;

&lt;p&gt;A Sanity Blueprint deploys four Scheduled Functions to perform those checks.&lt;/p&gt;

&lt;p&gt;But finding a title is not permission to publish it.&lt;/p&gt;

&lt;p&gt;When Book Club Watch finds a potential selection, it creates a structured bookClubDiscovery document instead of creating a live book document.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F36vzatl2d4p9k74hflfo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F36vzatl2d4p9k74hflfo.png" alt=" " width="628" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The automation proposes.&lt;/p&gt;

&lt;p&gt;The editor decides.&lt;/p&gt;

&lt;p&gt;Another Sanity Function reacts to that editorial transition.&lt;/p&gt;

&lt;p&gt;That distinction became one of the most important architectural decisions in Everlogue.&lt;/p&gt;

&lt;p&gt;The catalog can grow itself without allowing an automated scraper to define shared truth.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7tlppx1wetnr2b18ywju.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7tlppx1wetnr2b18ywju.png" alt=" " width="348" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The resulting catalog is also what powers the rest of Everlogue, including discovery surfaces and Ask Everlogue. I wanted the reading companion to feel like talking to a thoughtful friend in a bookshop, but its recommendations should come from books Everlogue actually knows about — not an invented shelf.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.everlogue.app" rel="noopener noreferrer"&gt;https://www.everlogue.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rather than a video walkthrough, I documented the workflow with screenshots showing the public app and the Sanity system behind it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuove3wlyqqh5adfr3zrz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuove3wlyqqh5adfr3zrz.png" alt=" " width="800" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvptrkd2hmq9d023jmkgj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvptrkd2hmq9d023jmkgj.png" alt=" " width="800" height="574"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fosgoltzic93qfi75c42l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fosgoltzic93qfi75c42l.png" alt=" " width="800" height="576"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5igbcgj3m8hqwsd4dnd1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5igbcgj3m8hqwsd4dnd1.png" alt=" " width="772" height="772"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp644dk0i9585pa4xa20j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp644dk0i9585pa4xa20j.png" alt=" " width="799" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8ift170s0yr53l3pvx37.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8ift170s0yr53l3pvx37.png" alt=" " width="800" height="685"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/sphcastillo/everlogue" rel="noopener noreferrer"&gt;https://github.com/sphcastillo/everlogue&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Book Club Watch infrastructure begins in:&lt;/p&gt;

&lt;p&gt;sanity.blueprint.ts&lt;/p&gt;

&lt;p&gt;The Blueprint defines:&lt;/p&gt;

&lt;p&gt;1 Book Club Watch robot token&lt;br&gt;
4 Scheduled Functions&lt;br&gt;
1 approval-triggered Document Function&lt;/p&gt;

&lt;p&gt;The core Watch implementation lives under:&lt;/p&gt;

&lt;p&gt;src/lib/book-club-watch/&lt;/p&gt;

&lt;p&gt;That code models club schedules, discovery state, source inspection, duplicate protection, approval state, and publishing.&lt;/p&gt;

&lt;p&gt;Sanity Studio adds the editorial layer: Review Queue, approved/publication failures, rejected discoveries, published history, run history, and custom actions for the review process.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Build Process
&lt;/h2&gt;

&lt;p&gt;I built Everlogue with the help of Codex &amp;amp; Cursor, and the most productive prompts were not "build me a Goodreads clone."&lt;/p&gt;

&lt;p&gt;They were small jobs with explicit constraints.&lt;/p&gt;

&lt;p&gt;Things like:&lt;/p&gt;

&lt;p&gt;Parse this Goodreads CSV, preserve the reader's shelf state, and do not create duplicate books we already own.&lt;/p&gt;

&lt;p&gt;Check this official book-club source and create a discovery when something changes.&lt;/p&gt;

&lt;p&gt;Never create a live book directly from the scheduled function.&lt;/p&gt;

&lt;p&gt;Record the Watch run even when nothing changed.&lt;/p&gt;

&lt;p&gt;Those constraints mattered because some of my first approaches were wrong.&lt;/p&gt;

&lt;p&gt;The first bad assumption: one monthly cron&lt;/p&gt;

&lt;p&gt;My first instinct was one Scheduled Function that ran once a month.&lt;/p&gt;

&lt;p&gt;That did not survive contact with reality.&lt;/p&gt;

&lt;p&gt;The four clubs have different announcement patterns, and Oprah does not have a dependable monthly date at all.&lt;/p&gt;

&lt;p&gt;Instead of forcing those sources into one schedule, I modeled the release behavior itself as structured configuration and let each watcher decide whether the current check was meaningful.&lt;/p&gt;

&lt;p&gt;The second bad assumption: discovery equals publication&lt;/p&gt;

&lt;p&gt;Another early direction collapsed:&lt;/p&gt;

&lt;p&gt;"We found the book"&lt;/p&gt;

&lt;p&gt;into:&lt;/p&gt;

&lt;p&gt;"Create the book"&lt;/p&gt;

&lt;p&gt;I did not want that.&lt;/p&gt;

&lt;p&gt;A title extracted from a webpage is evidence, not editorial truth.&lt;/p&gt;

&lt;p&gt;I split the process into two systems.&lt;/p&gt;

&lt;p&gt;Discovery proposes structured content.&lt;/p&gt;

&lt;p&gt;Editorial approval publishes structured content.&lt;/p&gt;

&lt;p&gt;A discovery moves through states such as:&lt;/p&gt;

&lt;p&gt;discovered&lt;br&gt;
   ↓&lt;br&gt;
needs_review&lt;br&gt;
   ↓&lt;br&gt;
approved ─────→ published&lt;br&gt;
   │&lt;br&gt;
   └──────────→ rejected&lt;/p&gt;

&lt;p&gt;Studio became the place where the human can inspect the title, authors, source URL, evidence, proposed metadata, existing-book matches, and publication mode.&lt;/p&gt;

&lt;p&gt;Only approval allows the publishing function to react.&lt;/p&gt;

&lt;p&gt;The publish trigger also checks that the discovery does not already have a catalog book, giving the workflow another layer of duplicate protection.&lt;/p&gt;

&lt;p&gt;Going past Studio&lt;/p&gt;

&lt;p&gt;The Blueprint became:&lt;/p&gt;

&lt;p&gt;Book Club Watch&lt;br&gt;
├── Robot Token&lt;br&gt;
├── Reese Scheduled Function&lt;br&gt;
├── GMA Scheduled Function&lt;br&gt;
├── Read With Jenna Scheduled Function&lt;br&gt;
├── Oprah Scheduled Function&lt;br&gt;
└── Publish Approved Discovery&lt;/p&gt;

&lt;p&gt;This is where I hit one of the biggest implementation problems.&lt;/p&gt;

&lt;p&gt;My initial Blueprint stack was project-scoped.&lt;/p&gt;

&lt;p&gt;The functions passed local tests, the Studio built, and the bundles compiled — but Sanity refused the deployment because Scheduled Functions require an organization-scoped Blueprint stack.&lt;/p&gt;

&lt;p&gt;I had to promote the stack from project scope to organization scope before the cron resources could deploy.&lt;/p&gt;

&lt;p&gt;That was one of the places where the AI-generated implementation was technically plausible but incomplete. The deployment environment exposed the missing constraint.&lt;/p&gt;

&lt;p&gt;Local testing also fooled me&lt;/p&gt;

&lt;p&gt;Another confusing moment came after a local test successfully found an Oprah selection.&lt;/p&gt;

&lt;p&gt;The terminal showed a real result, but my Studio Review Queue was empty.&lt;/p&gt;

&lt;p&gt;The reason was in my own runtime:&lt;/p&gt;

&lt;p&gt;if (context.local) {&lt;br&gt;
  console.log(await discoverPicks(...))&lt;br&gt;
  return&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Local Function tests intentionally exercised discovery without mutating the dataset.&lt;/p&gt;

&lt;p&gt;That forced me to separate two things I had initially treated as the same:&lt;/p&gt;

&lt;p&gt;Does source discovery work?&lt;/p&gt;

&lt;p&gt;and:&lt;/p&gt;

&lt;p&gt;Does the deployed cloud workflow write the correct editorial state?&lt;/p&gt;

&lt;p&gt;I then tested the actual deployed cron against a development dataset.&lt;/p&gt;

&lt;p&gt;That run discovered Little Wonder by Sophie Chen Keller from an official Oprah source and created a real bookClubDiscovery with:&lt;/p&gt;

&lt;p&gt;bookClub&lt;br&gt;
discoveredTitle&lt;br&gt;
discoveredAuthors&lt;br&gt;
selectionMonth&lt;br&gt;
selectionDate&lt;br&gt;
sourceUrl&lt;br&gt;
sourceEvidence&lt;br&gt;
identity&lt;br&gt;
matchExplanation&lt;br&gt;
status&lt;/p&gt;

&lt;p&gt;Its status was needs_review, exactly as intended.&lt;/p&gt;

&lt;p&gt;Development before production&lt;/p&gt;

&lt;p&gt;I eventually standardized Everlogue around two datasets:&lt;/p&gt;

&lt;p&gt;development&lt;br&gt;
production&lt;/p&gt;

&lt;p&gt;Book Club Watch was first enabled against development.&lt;/p&gt;

&lt;p&gt;For testing, I temporarily accelerated the Oprah schedule so I could observe a real cloud execution instead of waiting several days for the normal schedule.&lt;/p&gt;

&lt;p&gt;Once I verified the full flow, I restored Oprah's real Monday / Wednesday / Friday schedule.&lt;/p&gt;

&lt;p&gt;Before enabling Watch against production, I exported the entire production dataset — 1,644 documents and 327 assets — and added another Blueprint safeguard:&lt;/p&gt;

&lt;p&gt;Production cannot run enabled&lt;br&gt;
unless WATCH_PRODUCTION_VALIDATED=true&lt;/p&gt;

&lt;p&gt;Only after the development workflow passed did I deploy the production version.&lt;/p&gt;

&lt;p&gt;Failure states became part of the product&lt;/p&gt;

&lt;p&gt;The final system does not pretend external systems always work.&lt;/p&gt;

&lt;p&gt;Book Club Watch records run history and distinguishes outcomes such as:&lt;/p&gt;

&lt;p&gt;no change&lt;br&gt;
already recorded&lt;br&gt;
skipped&lt;br&gt;
failed&lt;br&gt;
discovered&lt;/p&gt;

&lt;p&gt;Studio keeps rejected discoveries, publication failures, and publishing history instead of hiding them.&lt;/p&gt;

&lt;p&gt;One of the live sources returned an HTTP 500 during testing. Google Books enrichment was also unavailable during one discovery.&lt;/p&gt;

&lt;p&gt;Neither condition was allowed to silently create bad catalog data.&lt;/p&gt;

&lt;p&gt;The result is the part of Everlogue I like most architecturally:&lt;/p&gt;

&lt;p&gt;automation can inspect the web and propose structured state, but it does not have unilateral authority over the shared catalog.&lt;/p&gt;

&lt;p&gt;The machine proposes.&lt;/p&gt;

&lt;p&gt;The editor decides.&lt;/p&gt;

&lt;p&gt;Sanity connects the two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sanity Project Details
&lt;/h2&gt;

&lt;p&gt;Project ID: 3h0o1unw&lt;/p&gt;

&lt;p&gt;Production dataset: production&lt;/p&gt;

&lt;p&gt;The production dataset is private because Everlogue contains reader-specific data and internal editorial state.&lt;/p&gt;

&lt;p&gt;Book Club Watch was validated against the separate development dataset before production enablement.&lt;/p&gt;

&lt;p&gt;Sanity is used for much more than page content in Everlogue. It holds the structured book catalog and editorial content while Studio, Scheduled Functions, Document Functions, Blueprints, and custom document actions form the operational layer around that content.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>sanitychallenge</category>
      <category>sanity</category>
      <category>ai</category>
    </item>
    <item>
      <title>Everlogue’s Reading Companion - a book recommendation agent grounded in structured Sanity</title>
      <dc:creator>Sophia Castillo</dc:creator>
      <pubDate>Sun, 04 Oct 2026 23:27:32 +0000</pubDate>
      <link>https://dev.to/sphcastillo/everlogues-reading-companion-a-book-recommendation-agent-grounded-in-structured-sanity-43b</link>
      <guid>https://dev.to/sphcastillo/everlogues-reading-companion-a-book-recommendation-agent-grounded-in-structured-sanity-43b</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/sanity-2026-09-16"&gt;Sanity Challenge, Path One: Ship an Agent That Queries Real Content&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Everlogue is a home for everything you read.&lt;/strong&gt; It brings together a book catalog, private reading shelves, celebrity book-club selections, and &lt;strong&gt;Ask Everlogue&lt;/strong&gt;, a Reading Companion grounded in structured Sanity content.&lt;/p&gt;

&lt;p&gt;The catalog began with selections from four clubs: &lt;strong&gt;Reese’s Book Club, Oprah’s Book Club, Read with Jenna, and GMA Book Club.&lt;/strong&gt; From there, it grows through its readers.&lt;/p&gt;

&lt;p&gt;Readers can import their Goodreads CSV files to bring over their &lt;strong&gt;Want to Read, Currently Reading, and Read&lt;/strong&gt; shelves. They can also search for a book and add it to an Everlogue shelf. If that book isn’t already in the catalog, the addition creates a request for review. Once reviewed and approved, the book joins the shared catalog and becomes available for discovery and recommendations.&lt;/p&gt;

&lt;p&gt;That growing catalog is the foundation of Everlogue’s Reading Companion. I wanted it to feel like talking to a thoughtful friend in a bookshop—someone who listens to what you want today and helps you find something you’ll enjoy.&lt;/p&gt;

&lt;p&gt;When a reader asks, &lt;strong&gt;“What should I read next?”&lt;/strong&gt;, the companion starts with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What are you in the mood for—or what’s a book you loved and want something similar to?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Their answer guides the search across the full approved Everlogue catalog, including suitable books on their Want to Read shelf. For signed-in readers, books already read or currently being read are excluded. The companion returns three recommendations with short, personal, spoiler-free explanations, grounded in books that actually exist in Everlogue.&lt;/p&gt;

&lt;p&gt;For readers who want to explore a book further, I established a &lt;strong&gt;Sanity Context Knowledge Base&lt;/strong&gt; using six website sources containing book-club announcements, summaries, and author interviews. I reviewed the generated entries covering book premises, themes, club selections, and author conversations, checked their citations, and made corrections.&lt;/p&gt;

&lt;p&gt;I then configured a dedicated MCP endpoint, &lt;strong&gt;Everlogue Book Knowledge&lt;/strong&gt;, so the companion could retrieve supporting material for questions about books. Its instructions require sources for factual claims, discourage unnecessary spoilers, and keep the conversation warm and natural. I selected &lt;strong&gt;OpenAI gpt-5.4-mini&lt;/strong&gt; as the starting model.&lt;/p&gt;

&lt;p&gt;The two sources serve different purposes: &lt;strong&gt;the structured catalog determines which books the companion can recommend, while the knowledge base supports deeper, source-backed conversations about them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As readers contribute more books and those additions are approved, Everlogue’s catalog grows—and the Reading Companion gains more possibilities to help someone find their next read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.everlogue.app/" rel="noopener noreferrer"&gt;https://www.everlogue.app/&lt;/a&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/sphcastillo/everlogue" rel="noopener noreferrer"&gt;https://github.com/sphcastillo/everlogue&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The companion lives in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/lib/companion-agent.ts&lt;/code&gt; — recommendation path vs Sanity Context MCP path&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/lib/companion-catalog.ts&lt;/code&gt; — GROQ catalog search, shelf exclusions, Want to Read&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/lib/companion-selection.ts&lt;/code&gt; — interpret the request, pick three supported candidates&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/app/api/companion/route.ts&lt;/code&gt; — authenticated shelf context, then the agent&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docs/reading-companion.md&lt;/code&gt; — how Context and recommendations are wired&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I Used Sanity
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Building the Knowledge Base
&lt;/h3&gt;

&lt;p&gt;I added six website sources containing book-club announcements, summaries, and interviews to Sanity Context. The generated entries covered book premises, themes, selections, and author conversations. I reviewed the generated content, investigated citations, and made corrections rather than treating ingestion as the end of the editorial process.&lt;/p&gt;

&lt;p&gt;I created a dedicated &lt;strong&gt;Everlogue Book Knowledge&lt;/strong&gt; MCP endpoint. The companion’s instructions emphasize factual support, a warm personal tone, and avoiding unnecessary spoilers.&lt;/p&gt;

&lt;p&gt;For Knowledge Base questions, the server connects through an MCP client and gives the model the tools exposed by the endpoint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;initial_context&lt;/code&gt; establishes how to navigate the Knowledge Base.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;knowledge_base_search&lt;/code&gt; finds relevant entries.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;knowledge_base_read&lt;/code&gt; retrieves supporting content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent must retrieve content before answering. Its instructions require it to ground book facts in that retrieval and cite source titles and URLs it actually used. Responses stream into the existing companion interface. The Context token stays on the server, and the MCP connection closes when the response finishes or is interrupted.&lt;/p&gt;

&lt;p&gt;In an end-to-end check, I asked about &lt;em&gt;The Christie Affair&lt;/em&gt;. The companion called the Context tools, identified Nina de Gramont, and returned a source-backed answer. I checked tool activity as well as the text of the response.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connecting knowledge to a reader’s library
&lt;/h3&gt;

&lt;p&gt;The companion uses two complementary content paths. Knowledge Base questions go through Sanity Context MCP. Recommendations query the structured catalog directly with GROQ through &lt;code&gt;@sanity/client&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That distinction matters: &lt;strong&gt;a book does not need an ingested article or Knowledge Source link to be recommended.&lt;/strong&gt; A reader-imported title can qualify through its catalog description and genres. Article-backed answers use retrieved sources; catalog-based recommendations do not invent article citations.&lt;/p&gt;

&lt;p&gt;Sanity stores the relationships that make personalization possible: books and genres, reader profiles, shelves and shelf entries, ratings, and reviews. Optional page-count and series information support explicit constraints when that information is available. The authenticated reader’s identity determines which shelf entries and feedback apply.&lt;/p&gt;

&lt;p&gt;The recommendation flow interprets the request, retrieves eligible candidates, selects up to three, and writes short explanations from the supplied metadata. It combines the reader’s current mood with structured eligibility checks and their own feedback. Missing metadata is not permission to invent a fact.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgolm2qhvqkp4djuv3jq4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgolm2qhvqkp4djuv3jq4.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fih4xekwxzq7tgs7c2ql7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fih4xekwxzq7tgs7c2ql7.png" alt=" " width="800" height="551"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw00ibpcdzgwmvnhr3aqr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw00ibpcdzgwmvnhr3aqr.png" alt=" " width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbmselota6xlmnrco3dab.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbmselota6xlmnrco3dab.png" alt=" " width="800" height="886"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What I worked through while building
&lt;/h3&gt;

&lt;p&gt;Much of the work was deciding what “helpful” should mean. We refined rules around low ratings, current preferences, series order, repeated suggestions, and the difference between a contextual book mention and a request for something similar.&lt;/p&gt;

&lt;p&gt;I also kept the model and added measurement: per-question token counts, cached input, estimated cost, duration, and outcomes are saved in Sanity. Daily usage limits and smaller, relevant context help prepare the companion for visitors. Two live test questions recorded about $0.015 combined in estimated model cost—a small test sample, not a forecast.&lt;/p&gt;

&lt;p&gt;Testing the surrounding experience mattered too. A first-sign-in failure exposed a profile-resolution problem after Clerk authentication. We changed setup to use the profile returned by Sanity’s creation transaction and made retry lookups bypass request memoization. We also moved homepage personalization behind separate loading boundaries and parallelized independent queries, keeping library data fresh.&lt;/p&gt;

&lt;p&gt;The final hurdle was to make it clear what circumstances the reader companion should ask the knowledge base over the catalog. Also, we had to make sure source names and URLS are stripped from replies, and strip filler like “the book” from catalog lookups so “Tell me about the book The House in the Pines” searches the real title instead of a phrase that isn’t in the catalog.  &lt;/p&gt;

&lt;p&gt;I know I'll still be tinkering with it, but I'm currently really happy with today's Ask Everlogue. &lt;/p&gt;

&lt;h2&gt;
  
  
  Sanity Project Details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Project ID:&lt;/strong&gt; &lt;code&gt;3h0o1unw&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dataset:&lt;/strong&gt; &lt;code&gt;production&lt;/code&gt; (private)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Organization Context endpoint:&lt;/strong&gt; Everlogue Book Knowledge (&lt;code&gt;everlogue-book-knowledge&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dataset contains reader information, so I am sharing the project ID rather than making the dataset public. The Context endpoint uses a server-side organization token with Context → Viewer permission.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agent Session
&lt;/h2&gt;

&lt;p&gt;I worked with Codex &amp;amp; Cursor to turn the reading experience into implementation rules, connect the MCP endpoint, verify retrieval, and test edge cases. The most useful parts of that process were tracing where an answer came from and deciding what the companion should do when information was missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credits
&lt;/h2&gt;

&lt;p&gt;Built with Sanity Content Lake, GROQ, Sanity Studio, Sanity Context, next-sanity, the Vercel AI SDK MCP client, OpenAI &lt;code&gt;gpt-5.4-mini&lt;/code&gt;, Clerk, and Next.js.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>sanitychallenge</category>
      <category>sanity</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
