# Install Reading Shelf on an existing website

Human guide: https://lilygeidelberg.com/reading/

This guide is for an agent asked to add Reading Shelf to its user's website. The component displays current books and expandable finished history, starting with an icon-only book with a turning gold page and a pulsing arrow. It is a dependency-free browser web component, not a hosted multi-user service.

## Assets and contract

- Component: https://lilygeidelberg.com/reading/reading-shelf.js
- Editable starter JSON: https://lilygeidelberg.com/reading/example-shelf.json
- API contract: https://lilygeidelberg.com/reading/openapi.json
- Read-only demonstration of Lily's books: https://lilygeidelberg.com/api/reading

The demonstration feed and Lily's editor are not an installation destination for someone else's books. Do not send writes to Lily's API. The default installation below needs no key, account, database or new hosting service.

## Goodreads public-profile integration

If the user already updates Goodreads, offer its **read-only public RSS connection** instead of maintaining a second book list. Ask for their Goodreads **user profile** URL (`https://www.goodreads.com/user/show/USER_ID-name`) or numeric user ID. A book ID or author ID is not a user ID. Never request a Goodreads password, session cookie or private RSS key. The profile must be public; do not bypass private-profile restrictions.

1. Read `https://lilygeidelberg.com/api/reading/goodreads/feed?user=USER_ID`, replacing the placeholder with the numeric user ID. It returns the same Shelf JSON schema, with public CORS, or an explicit error. This operation does not modify Lily's shelf or the Goodreads profile. Check the returned name and books with the user before publishing it.
2. Keep the component unchanged and use that URL as its `src`:

```html
<script type="module" src="https://lilygeidelberg.com/reading/reading-shelf.js"></script>
<reading-shelf collapsible src="https://lilygeidelberg.com/api/reading/goodreads/feed?user=YOUR_GOODREADS_USER_ID"></reading-shelf>
```

3. Changes flow **Goodreads → website only**. Upstream feeds are cached for one hour, plus up to five minutes of HTTP response caching; Goodreads' own RSS cache can delay changes further. The widget's 30-second polling is not a guarantee of 30-second Goodreads updates. Do not describe this as OAuth, two-way sync, real-time presence or the retired Goodreads developer API.
4. The adapter reads `currently-reading` and `read`, showing at most 24 current books and 100 recent finished books, sorted by Goodreads' read date. This is a recent shelf, not a full-library export. `source.currentTruncated` and `source.historyMayBeTruncated` flag limits. Goodreads reviews, descriptions and ratings are deliberately excluded. `startedAt` and current progress remain `null`; missing finish dates stay `null`. Do not substitute the date a book was added for the date it was read. Goodreads book IDs stay in stable `goodreads-ID` record IDs; `workId` remains empty because they are not Open Library work IDs.
5. The hosted adapter is a public convenience endpoint, not an account service or availability guarantee. If it fails, the widget keeps already-loaded data. On a fresh visit it needs a successful response or an explicitly maintained fallback. For an independent deployment, implement the same server-side RSS-to-JSON adapter at the user's origin; direct browser requests to Goodreads may fail CORS. Restrict upstream requests to `https://www.goodreads.com/review/list_rss/{validated numeric user ID}` with fixed shelf, sort and limit parameters. Reject redirects; bound request time, body size and XML depth; use a strict XML parser with DTD/entity expansion disabled. Cache requests at least an hour. Never return login/challenge HTML as an empty shelf.

On this site's **owner editor**, “Connect Goodreads” previews the profile before changing the live source. The manual shelf remains saved separately in the existing D1 document. While connected, the public endpoint serves a durable last-successful Goodreads snapshot and manual PATCH actions return `409`. Visitor reads schedule a refresh when the hourly check is due; a failed refresh keeps the snapshot. Disconnecting restores the exact original manual books; it does **not** import or merge Goodreads history into them. Books added on Goodreads during the connection remain on Goodreads. The “Download reading history” button can export the currently displayed snapshot.

For an agent managing a user's own deployment, `GET /api/reading/goodreads` returns connection status (owner authentication required). `POST` with `{ "action": "preview", "profile": "USER_PROFILE_URL" }` is read-only and returns a preview. Confirm the intended profile/source change with the user, then POST `connect` with `profile` and the current quoted revision in `If-Match`. POST `sync` checks for updates; DELETE disconnects. All these owner operations require authentication; changing the connection also requires revision matching. Never connect somebody else's profile to Lily's editor. See the OpenAPI contract for exact payloads.

## Default installation: the user's own JSON file

1. Inspect the existing website, its instructions, framework, public asset directory and hosting flow. Preserve its design and deployment setup. Ask where to place the shelf only if the intended location is unclear.
2. Get the user's current books and any finished books they want to include. Titles and authors are sufficient. Ask for missing details; do not invent titles, authors, dates, progress or history. Do not publish the starter's placeholder book as the user's real reading list.
3. Create a public `reading.json` file on the user's website using the starter format. Assign each book a unique, stable `id`. Set `status` to `reading` or `finished`. Keep unknown optional values empty or `null`. If no books are supplied yet, use `books: []`.
4. Load the component once in the browser and put a `reading-shelf` element in the chosen location. This HTML is the simplest integration:

```html
<script type="module" src="https://lilygeidelberg.com/reading/reading-shelf.js"></script>
<reading-shelf collapsible src="/reading.json"></reading-shelf>
```

5. Adjust the feed URL and any self-hosted asset paths for the site's public base path (for example `/portfolio/reading.json` for a site under `/portfolio/`). You can instead download the component, retain its included artwork attribution, host the file on the user's site, and change the script URL to that path. The standalone component needs no package install or runtime dependencies. Its two-page SVG is inline, so no separate icon file is needed. Load it once even if the page has more than one shelf.
6. In a server-rendered framework, register the element on the client only. The module extends `HTMLElement`; do not evaluate it during server rendering. Adapt the loader to the existing framework rather than replacing the framework or adding an unnecessary build system. Plain HTML custom-element markup is enough for the rendered element.
7. Publish through the site's normal flow and perform the checks below. Explain to the user which file or workflow changes their books. Do not describe the file-based version as an automatically provisioned editor or account.

## Book data and maintenance

The shelf object has `schemaVersion: 1`, a non-negative integer `revision`, `name`, an ISO date-time `updatedAt` or `null`, and a `books` array. See the starter JSON and the `Shelf` schema in the API contract.

Each book has a stable `id`, `title`, `author` and `status`. Optional fields:

- `cover`: a public HTTPS image URL or a root-relative image path. Empty if unavailable. The widget handles missing covers.
- `url`: a public HTTPS book link or a root-relative link. Empty if unavailable.
- `workId`: an Open Library work ID such as `OL37564003W`, not an edition ID. Empty if unknown. Verify any ID rather than guessing it.
- `startedAt` and `finishedAt`: actual dates as `YYYY-MM-DD`, or `null` when unknown. A finished book needs a finish date for the write API; if a historical date is unknown, ask before importing it into that API.
- `progress`: a number from 0 to 100, or `null` to hide progress.
- `note`: an optional public thought, up to 280 characters.

Relative cover and book URLs resolve against the feed URL. Prefer root-relative paths when hosting all assets together. Use book artwork the user is entitled to use; do not substitute an invented cover for a real book.

For file-based edits, read the latest JSON before changing it and preserve unrelated books and history. Mark a completed book `finished`, set its actual `finishedAt`, and add the next book with a new ID and `status: "reading"`. Increase `revision` on every change and set `updatedAt` to the actual edit timestamp. Publish the changed JSON; changing an unpublished local file cannot update visitors' shelves.

The widget re-fetches while visible every 30 seconds and on return to the tab. It also exposes `.refresh()`. Hosting caches can delay a published update; use a cache policy that revalidates the feed or purge that file on publication. It does not write to the source and does not track whether the owner is physically reading or online.

The feed, book list and notes are public. Do not put credentials or private notes in them.

## Display options

- `collapsible`: start closed, showing only the book and arrow. The control has an accessible label. The gold page turns left, becomes grey as the right page becomes gold, and repeats; the arrow pulses. These animations run only while there are current books and the shelf is closed. All pause on hover/focus or in a hidden tab; reduced-motion preferences disable them.
- `open`: alongside `collapsible`, start expanded. The user can close it. Without `collapsible`, books stay visible.
- `variant="profile"`: a narrower, single-column layout.
- `fallback-src="/reading-backup.json"`: an optional backup feed. Keep it current. It is not a live backup service, and an old fallback does not overwrite a higher live revision already displayed.
- CSS custom properties: `--reading-ink`, `--reading-muted`, `--reading-line`, `--reading-background`, `--reading-font`; optional `--reading-hover` and `--reading-press` customise interaction washes.
- Properties/methods: `.data` accepts the shelf object, `.refresh()` re-fetches, `.open` controls disclosure. If supplying only `.data`, omit `src` so a fetch does not replace it.
- Events: `reading-change` contains the fetched shelf in `event.detail`; `reading-error` contains a user-safe error message. Both bubble across the shadow boundary. Setting `.data` directly does not emit `reading-change`.

Preserve the native button, its accessible name and expanded state, the 44-pixel touch target, keyboard focus handling and reduced-motion styles. Styles are isolated in shadow DOM; theme through the custom properties rather than relying on selectors reaching inside it.

## Hosting and browser checks

1. Open the published JSON URL directly. It must return a valid shelf, not a login screen, redirect to sign-in, SPA fallback HTML or a 404. Empty shelves are valid.
2. Confirm the component URL returns JavaScript, and load it as `type="module"`. If using a site builder, check that the chosen page supports custom scripts and a public feed.
3. Keep a self-hosted feed on the user's own origin where possible. A feed on a different origin must allow public cross-origin GET requests with `Access-Control-Allow-Origin`; the component fetches without credentials. This site's hosted component, demonstration feed and Goodreads adapter already allow cross-origin reads. See [MDN's CORS reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS).
4. Respect the site's Content Security Policy. A cross-origin component needs its host allowed by `script-src`; the feed needs `connect-src`; covers need their hosts in `img-src`. The component creates an inline `<style>` in its shadow root, so a strict policy also needs a reviewed allowance for that style (for example its exact CSP hash), or an adapted self-hosted version using an allowed stylesheet. Self-host a reviewed copy when using style hashes, so upstream changes do not invalidate the hash. Do not broadly disable an existing policy just to make the widget load. See [MDN's style-src reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/style-src).
5. Test the closed book-and-arrow control, opening and closing, finished-history expansion, narrow widths, keyboard focus and reduced motion. Confirm it is using the user's feed, not the demonstration feed.
6. Publish a known authorised book update and verify it appears after refresh. Verify that polling does not close an expanded shelf or history. Remove any labelled test data before handoff.

## Optional: a live editor and write API

Only add a backend/editor when the user requests that workflow. It is not needed for the static-file installation. Provision it on the user's own host with their own persistent storage and private editing access; do not point write calls at Lily's deployment.

Use the OpenAPI contract as the implementation reference. It describes one owner per deployment, a public `GET /api/reading`, and authenticated, revision-checked `PATCH /api/reading` actions. It is a specification, not a hosted account signup or a backend installer. A developer or agent must implement the user's API and editor separately.

For agent-side writes to an existing compatible API:

1. Read the user's latest shelf.
2. Use a write key obtained through that user's authorised secret storage. Keep it out of URLs, HTML, public JavaScript, JSON and logs.
3. Send the smallest requested action with `Authorization: Bearer …`, `Content-Type: application/json` and `If-Match` containing the quoted current revision. For revision `3`, the exact value is `"3"`; never hard-code an example revision.
4. Use a `start` action with `finishId` to finish one book and start another atomically. Other actions include `finish`, `add-finished`, `resume`, `update`, `remove` and revision-checked `restore`.
5. On `412`, read again and reconcile the user's requested change. Do not blindly retry an overwrite. Only report a save after the API confirms success.

Once this API is ready, point the widget's `src` at its public reading endpoint. The public widget never needs the owner's write key.
