Road to a book lexicon (Part 2)

The one where I actually define a lexicon


Welcome back!


In Part 1, we discussed the difference between works and editions, and given that editions are facts and works are opinions; we will base the lexicon on editions. We will do this in a somewhat clever way: we allow readers of the lexicon to construct their own meaning for a work, based on the record's identifiers. If I were to leave it at that, it'd be a "paint the rest of the owl" sort of vibe, so let's go ahead and build up what we think the rest of the lexicon should include.

The "how to draw the rest of the owl" meme with text over it that reads:

How to make a book lexicon
Just make it

In my ATmosphereConf presentation, I explained a thought experiment that I believe is central to how we should define all lexicons: "If BookHive disappeared tomorrow, is the data in your PDS still meaningful?" I called this the "day after" test, and it really is a test of user agency, and portability of the record. Instead of locking relevant fields like a book's cover image behind some API, I want for it to be owned by that user, in their PDS, not some external database or API that can be locked-down or "sunsetted". The goal of an interoperable lexicon in the first place is premised on the idea that the user's data should outlive whichever app wrote it. Don't have that old camera software? No worries, it's a JPEG.

A record that only means something while its provider is alive is not actually owned by the user

OK, I'll jump off my high-horse now.




The smallest shape that survives

We could sit down and debate every possible facet of a book, I'm pretty sure that bibliographers have been doing that for centuries and seem to be having a great time. But for interoperability, the question is different: what is the smallest shape that survives the day-after test? Whatever answers that question is the basis for what a book means across platforms. Everything else gets to be optional.

So, let’s go through them, field-by-field:

title

Nothing to see here, of course a book has a title and it should exist in the record.

identifiers

As discussed in Part 1, a book will have at least 1 identifier associated with it. The encouraged pattern is to essentially write any identifier that you have about this edition of the book into the record, in hopes that another system can match it later. It also is recommended to write your own internal identifier scoped to your application, so that you can have a quick lookup in your application. This would mean that every record in the network should be resolvable at least by the system that wrote the record. But, I'm getting ahead of myself here, let's see the other fields first.

An Identifier is composed of a (type, value) pair which identifies what system the identifier should be treated as a part of (e.g. as an ISBN) and the identifier itself, and it consists of:

  • value: the raw identifier, in its canonical format, as a string
    • Examples: 9780765348326, OL34709636M, bk_1234
  • type: which namespace the value is valid within. Well-known global schemes get a bare token; if a platform minted the ID, use the domain to namespace them
    • Examples: isbn, asin, hardcover.app, bookhive.buzz
  • url: an optional canonical URL to the resource in the system, for display and reference, not for comparison

contributors

We normally think of a book as having an author, but what about an anonymous book? Or, maybe it is an audiobook and you also want to attribute the narrator? Co-author? How about the illustrator? Books are not always a monolith made by a single person, so we consider an author a contributor, part of the whole (a big part!)

This is where the day-after test really comes into play, normally in a database you'd normalize and have this point to another record. But, like I said in my talk, the PDS is not a database! We don't want to have to hunt references to other records and have to maintain relationships between them.

A Contributor consists of:

  • name: The contributor's name as a single string
  • role: The nature of the contribution that was made, if omitted, "author" is assumed
    • Examples: author, co-author, editor, translator, narrator, illustrator, etc.
  • identifiers: optional external Identifiers for the Contributor, they have the same problem as books, we don't number people, so identifiers can help disambiguate John Smith from John Smith
  • ref: an optional AT-URI which is meant for enrichment, never required to display the book, and what it may point at is deliberately left open (not every author is on ATProto, and nobody should have to mirror them).

cover and coverUrl

I was really torn about this one. I love seeing all of the book covers on my BookHive profile... But, the reality is that we may not always have a cover image to attach to our book. So, while it is highly encouraged to fill out the cover with a blob, it is optional; and, for those serving over APIs and who want to send a single message, a URL to an image is also acceptable, but the preferred storage is as a blob for maximum user agency. Have both? Stuff 'em in, why not!

All together now...

type EditionRef = {
  title: string;
  identifiers: Identifier[];
  contributors: Contributor[];
  cover?: Blob;
  coverUrl?: string;
};

This makes the smallest form of the "I mean this book" object. Any other record (e.g. reviews, shelves, etc.) that needs to reference a book will use this shape to durably reference a book. It is always displayable, even if no provider recognizes any of its identifiers, and it stays resolvable for as long as anyone does.

The actual full-fat record

Without further ado, here is the lexicon I propose:

type Edition = {
  // Required
  title: string;
  identifiers: Identifier[];   // at least one
  contributors: Contributor[]; // may be empty
  createdAt: string;

  // Optional
  subtitle?: string;
  cover?: Blob;                // when the record lives in a repo
  coverUrl?: string;           // when it's served over the wire
  language?: string;           // BCP 47
  publishedDate?: string;      // ISO 8601
  publisher?: { name: string; identifiers?: Identifier[] };
  format?: "physical" | "ebook" | "audio";
  pages?: number;              // print & ebook
  durationSeconds?: number;    // audio
  chapters?: number;
  series?: string;
  seriesPosition?: string;     // "1.5" and "Vol. II" both happen
  bookEdition?: string;        // "2nd revised", "Abridged"
  description?: string;
};

I won't bore you by going through each and every one of these fields and why I named them this way - when I publish the lexicon it should become clearer. For the most part, the fields are taken directly from schema.org's definition of a Book, but in a more stripped-down fashion, since a lexicon easily allows adding optional fields later. I’d recommend that any new fields follow schema.org's existing schema where possible, to avoid bike-shedding.

Finally, a review lexicon

I didn't really expect to be writing about the review lexicon shape, at this stage, but I felt like it was a very good example of how an EditionRef achieves it's stated goal of interoperability.

type Review = {
  subject: EditionRef;             // match by identifiers
  createdAt: string;

  // At least one of these two
  rating?: number;                 // 1-20
  textContent?: string;            // plain text, no markdown

  subjectRecord?: string;         // at-uri to an edition; not for matching
  content?: { $type: string };    // stansite-compatible
  flags?: Flags;
};

type Flags = {
  spoilers?: boolean;             // mark as spoiler
};

A review has a subject, the resolvable EditionRef shape, and that is the primary way platforms determine which book a review refers to. A review has at least a star rating, or a written review textContent. Given that different platforms will have different ways of describing rich-text, I will defer this to be a client-specific field just like standard.site does.

The most important thing to note is that a review is completely self-describing: what book it refers to, and the review content itself. Let me prove that with an example record:

{
  "$type": "community.lexicon.book.review",
  "subject": {
    "title": "Dune",
    "contributors": [{ "name": "Frank Herbert" }],
    "identifiers": [
      { "type": "isbn", "value": "9780441172719" },
      {
        "type": "bookhive.buzz",
        "value": "bk_GUShjG8U9l93XqIrGiKV",
        "url": "https://bookhive.buzz/books/bk_GUShjG8U9l93XqIrGiKV"
      }
    ],
    "coverUrl": "https://bookhive.buzz/images/books/bk_GUShjG8U9l93XqIrGiKV",
    "cover": {
      "ref": {
        "$link": "bafkreievrsqw3akkjoruc37rzrhfm34s6kwxyqkrozbpfu2u3uvrt4x4cm"
      },
      "size": 127075,
      "$type": "blob",
      "mimeType": "image/jpeg"
    }
  },
  "subjectRecord": "at://did:plc:enu2j5xjlqsjaylv3du4myh4/community.lexicon.book.edition/3lwq2xk5em25s",
  "rating": 16,
  "textContent": "The spice must flow. A slow first act pays off completely once Paul reaches the desert. The ecology chapters are the best part, and the ending recontextualizes everything before it.",
  "flags": { "spoilers": true },
  "createdAt": "2026-08-28T04:15:00.000Z"
}

Isn't that just grand? We can tell which book (title & author), display it (even with a cover url & link to the original URL), the user's rating of that book (out of 20, to allow 1/4 precision of stars) and read the review itself, spoiler-gated! This self-contained record allows a user to post multiple reviews for a book if they so wish (e.g. on re-reading) anyone not on BookHive can resolve the ISBN on the record and map it into their own system.

Note, that cover blob lives in the reviewer’s repo, so even if BookHive and its image CDN disappear tomorrow, the cover survives in the user’s own PDS. It really is a JPEG. Also, that bookhive.buzz identifier is written for every edition of Dune in BookHive’s catalog, so anyone can reconstruct BookHive’s opinion of the work from public records. Another platform appends its own identifier to the list, and that is all it takes to map a book from one platform to another!

One more thing...

We aren't finished yet though, we have a book & review lexicon, but, I don't think we have true interoperability until you can take your data and move it around. It's like having a file, but not having a way to export or import it. Remember “every record is resolvable by at least the system that wrote it”? Part 3 is about making it resolvable by everyone else.

See you in Part 3!



2
2