Library design: cards + redesigned list on the bookmarks screen, tag picker sheet #9

Merged
admin merged 11 commits from feat/library-browse-prototype into master 2026-07-30 04:03:03 +00:00
Owner

What this changes

The bookmarks screen gets a second layout and a new visual language, and the tab bar loses a tab.

User-visible:

  • A cards layout on the bookmarks screen — color blocks on paper instead of rows. Toggle in the toolbar, choice persists.
  • The list layout redesigned to match: paper ground, color chip where the favicon was, serif title, monospaced meta, wrapped tag chips. Everything the old row carried is still there — unread state, AI summary, tags, podcast button, reading progress, and all swipe actions.
  • A browser-tab strip of tag filters above both layouts, filtering server-side.
  • The Tags tab is gone; it's now a searchable sheet from the strip's +. Picking a tag pins it as a filter tab.
  • Light and dark, both first-class.

Not changed: Sources, Podcasts, Search, and the Tags/Search screens' row style (BookmarkListRow takes a style, so redesigning the bookmarks screen didn't silently restyle the others).

What the real data decided

Built against the live library (300 bookmarks sampled), which vetoed several parts of the reference design outright:

Reference assumed Real data Resolution
Publication-year stamp date_added is 2025 or 2026 for every bookmark Domain takes the slot, reduced to registered form — 92 of 300 hosts overflow the card's one monospaced line
~30-char headline titles Median 66, p90 159, max 332 Cards keep the name before the colon; the list carries the full title
Every item has a title 15 of 300 fall back to the raw URL Rendered unquoted, with the URL path in the stamp slot
Color as identifier 96 tags into 12 swatches Hash the primary tag, not the domain — 128 of 300 are github.com and would have painted half the library one color

The strongest signal: not one of the 300 has a preview_image_url. A thumbnail grid would render 300 empty boxes. Color blocks are the right primitive for this data.

A 4-column grid was cut for the same reason 3 columns later became 2: at a given type size the column count is set by how many characters fit before titles truncate mid-word.

Dark mode

Not an inversion. The ground keeps the palette's warm cast (brown-black, not neutral). The dark swatches aren't the light ones dimmed uniformly — the pale end (shell, blush, pale blue) drops a long way so it doesn't glare as bright slabs, and the dark end (forest, navy) comes up so it doesn't vanish. Both converge on a mid band, which is what keeps twelve swatches distinguishable in either scheme. Text-on-swatch resolves per scheme, since a swatch can be light in one and mid in the other.

Structure

  • LibraryKit.swift — the shared vocabulary: paper palette, Bookmark → LibraryItem, the card, the filter strip, and a small FlowLayout for wrapping tags. Both the shipping screen and the standalone prototype draw from it, so they can't drift.
  • BookmarkActions.swift — the context menu and podcast launch path, extracted out of BookmarkListRow. Cards and rows offer identical actions because they are the same code.
  • LibraryGridView.swift / LibraryListRow.swift — the two presentations.
  • Views/Prototypes/LibraryView.swift — the standalone design surface with sample data, for Previews.

Removals

  • The Tags tab and AppTab.tags.
  • TagBookmarksView — a filter tab now shows the same bookmarks in whichever layout you're already in, so it was dead weight.

Verification

Built and run on an iPhone 17 Pro simulator (iOS 26.2) against the live linkding server, in both layouts and both color schemes. Unit suite passes (16 tests).

Interaction was verified by driving the real UI with a temporary XCUITest target, removed before commit because both cases need the live server to have loaded bookmarks first:

  • Tapping a card opens the browser (this caught a real bug — see below).
  • + opens the tag sheet; picking ai pinned an ai tab.
  • The tab bar reads Bookmarks / Sources / Podcasts / Search.
  • The sheet lists 42 tags where the old inline menu managed 21.

Bugs found and fixed along the way

  • Tapping a card did nothing. LibraryCard carried its own press effect via onLongPressGesture(minimumDuration: 0), which fires on touch-down and swallows the host's tap. Invisible in the prototype because nothing was listening.
  • The + menu couldn't reach most tags. It listed tags on the loaded page — 21 of 96 — and with a filter active collapsed to only co-occurring tags, making a second unrelated tag impossible to pin. The sheet sources from the tags endpoint.
  • The + had no accessibility label, so VoiceOver announced nothing.

Known limits

  • cardTitle's colon heuristic is tuned on a GitHub-heavy library (128 of 300). The durable version is an aiHeadline from the existing ClaudeService.enrich pass; this needs no enrichment.
  • At 300+ items color is texture, not an index — it groups rather than names.
  • The list's tag row caps at two lines, so the heaviest bookmarks (5 linkding tags + AI tags) hide the tail rather than truncating it.
  • Tag counts in the sheet come from loaded bookmarks, so a tag not yet paged in shows no number rather than a wrong one.

Unrelated bug spotted, not touched

SpotlightIndexer throws for every donated item: Provided object for field url is of class NSURL, expected class: NSString. Spotlight indexing is silently failing for all bookmarks.


🤖 Generated with Claude Code

https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM

## What this changes The bookmarks screen gets a second layout and a new visual language, and the tab bar loses a tab. **User-visible:** - A **cards layout** on the bookmarks screen — color blocks on paper instead of rows. Toggle in the toolbar, choice persists. - The **list layout redesigned** to match: paper ground, color chip where the favicon was, serif title, monospaced meta, wrapped tag chips. Everything the old row carried is still there — unread state, AI summary, tags, podcast button, reading progress, and all swipe actions. - A **browser-tab strip of tag filters** above both layouts, filtering server-side. - The **Tags tab is gone**; it's now a searchable sheet from the strip's `+`. Picking a tag pins it as a filter tab. - **Light and dark, both first-class.** **Not changed:** Sources, Podcasts, Search, and the Tags/Search screens' row style (`BookmarkListRow` takes a `style`, so redesigning the bookmarks screen didn't silently restyle the others). ## What the real data decided Built against the live library (300 bookmarks sampled), which vetoed several parts of the reference design outright: | Reference assumed | Real data | Resolution | |---|---|---| | Publication-year stamp | `date_added` is 2025 or 2026 for **every** bookmark | Domain takes the slot, reduced to registered form — 92 of 300 hosts overflow the card's one monospaced line | | ~30-char headline titles | Median **66**, p90 **159**, max **332** | Cards keep the name before the colon; the list carries the full title | | Every item has a title | **15 of 300** fall back to the raw URL | Rendered unquoted, with the URL path in the stamp slot | | Color as identifier | 96 tags into 12 swatches | Hash the **primary tag**, not the domain — 128 of 300 are github.com and would have painted half the library one color | **The strongest signal:** not one of the 300 has a `preview_image_url`. A thumbnail grid would render 300 empty boxes. Color blocks are the right primitive for *this* data. A 4-column grid was cut for the same reason 3 columns later became 2: at a given type size the column count is set by how many characters fit before titles truncate mid-word. ## Dark mode Not an inversion. The ground keeps the palette's warm cast (brown-black, not neutral). The dark swatches aren't the light ones dimmed uniformly — the pale end (shell, blush, pale blue) drops a long way so it doesn't glare as bright slabs, and the dark end (forest, navy) comes **up** so it doesn't vanish. Both converge on a mid band, which is what keeps twelve swatches distinguishable in either scheme. Text-on-swatch resolves per scheme, since a swatch can be light in one and mid in the other. ## Structure - **`LibraryKit.swift`** — the shared vocabulary: paper palette, `Bookmark → LibraryItem`, the card, the filter strip, and a small `FlowLayout` for wrapping tags. Both the shipping screen and the standalone prototype draw from it, so they can't drift. - **`BookmarkActions.swift`** — the context menu and podcast launch path, extracted out of `BookmarkListRow`. Cards and rows offer identical actions because they *are* the same code. - **`LibraryGridView.swift` / `LibraryListRow.swift`** — the two presentations. - **`Views/Prototypes/LibraryView.swift`** — the standalone design surface with sample data, for Previews. ## Removals - The `Tags` tab and `AppTab.tags`. - `TagBookmarksView` — a filter tab now shows the same bookmarks in whichever layout you're already in, so it was dead weight. ## Verification Built and run on an iPhone 17 Pro simulator (iOS 26.2) against the live linkding server, in both layouts and both color schemes. Unit suite passes (16 tests). Interaction was verified by driving the real UI with a temporary XCUITest target, removed before commit because both cases need the live server to have loaded bookmarks first: - Tapping a card opens the browser (this caught a real bug — see below). - `+` opens the tag sheet; picking `ai` pinned an `ai` tab. - The tab bar reads `Bookmarks / Sources / Podcasts / Search`. - The sheet lists **42 tags where the old inline menu managed 21**. ## Bugs found and fixed along the way - **Tapping a card did nothing.** `LibraryCard` carried its own press effect via `onLongPressGesture(minimumDuration: 0)`, which fires on touch-down and swallows the host's tap. Invisible in the prototype because nothing was listening. - **The `+` menu couldn't reach most tags.** It listed tags on the loaded page — 21 of 96 — and with a filter active collapsed to only co-occurring tags, making a second unrelated tag impossible to pin. The sheet sources from the tags endpoint. - **The `+` had no accessibility label**, so VoiceOver announced nothing. ## Known limits - `cardTitle`'s colon heuristic is tuned on a GitHub-heavy library (128 of 300). The durable version is an `aiHeadline` from the existing `ClaudeService.enrich` pass; this needs no enrichment. - At 300+ items color is texture, not an index — it groups rather than names. - The list's tag row caps at two lines, so the heaviest bookmarks (5 linkding tags + AI tags) hide the tail rather than truncating it. - Tag counts in the sheet come from loaded bookmarks, so a tag not yet paged in shows no number rather than a wrong one. ## Unrelated bug spotted, not touched `SpotlightIndexer` throws for every donated item: `Provided object for field url is of class NSURL, expected class: NSString`. Spotlight indexing is silently failing for all bookmarks. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
admin added 1 commit 2026-07-26 06:31:09 +00:00
Add Library browse-screen prototype
CI / build-and-deploy (pull_request) Successful in 22s
CI / build-and-deploy (push) Successful in 52s
6a9b6f193f
An unreferenced SwiftUI prototype of a paper/serif/monospace browse screen
for bookmarks: color blocks instead of thumbnails, a browser-tab strip of
saved tag filters, and cards/list modes that morph between each other via
matchedGeometryEffect. Nothing in the app links to it yet.

Built against the real linkding library (300 bookmarks), which is what
shaped it:

- No bookmark has a preview_image_url, so there is nothing to put in a
  thumbnail grid. Color blocks are the right primitive for this data.
- date_added is 2025 or 2026 for every bookmark, so the reference design's
  year stamp carries no signal. The domain takes that slot instead,
  reduced to its registered form because 92 of 300 hosts overflow the
  card's one monospaced line.
- Real titles run 3x longer than the design assumes (median 66 chars,
  p90 159, max 332) and are mostly "name: what it does". Cards show the
  name, the list carries the whole title.
- 15 bookmarks have no scraped title at all and fall back to the raw URL;
  those render unquoted with the path in the stamp slot.
- Color is hashed from the primary tag, not the domain: 128 of 300 are
  github.com, which would paint half the library one color.

Both color schemes are first-class. The dark palette is not the light one
dimmed uniformly - pale swatches drop a long way so they do not glare and
dark swatches come up so they do not vanish, converging on a mid band that
keeps all twelve distinguishable in either scheme.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
admin added 1 commit 2026-07-26 06:41:25 +00:00
Wire the library presentation into BookmarksView
CI / build-and-deploy (pull_request) Successful in 20s
CI / build-and-deploy (push) Successful in 19s
99add346fd
Adds a cards/list toggle to the bookmarks screen. List mode is the
existing screen, untouched and still the default; cards mode is the
library presentation running on real bookmarks.

Structure:

- LibraryKit.swift holds what both the shipping screen and the prototype
  draw from — the paper palette, the Bookmark -> LibraryItem projection,
  the color card, and the tab strip. The prototype in Views/Prototypes
  keeps only its standalone chrome and sample data, so the two can no
  longer drift.
- BookmarkActions.swift extracts the context menu and the podcast launch
  path out of BookmarkListRow. Cards and rows now offer exactly the same
  actions because they are the same code. The menu is a @ViewBuilder
  rather than a ViewModifier: each host already owns the sheets it
  presents, and a modifier would have forced a second copy of that state.
- LibraryGridView.swift is the Bookmark-driven grid, with the same tap-to
  -open, long-press-for-menu, pagination and podcast sheets as the list.

Two behaviors worth calling out:

Tag tabs filter server-side through linkding's `#tag` search syntax.
Filtering the loaded page client-side would only ever search the most
recent 50 of 600+ bookmarks and quietly look empty; verified against the
server that `q=#dev-tools` returns 64 tagged results where `q=dev-tools`
returns 0.

Leaving cards mode clears any tag filter. The classic list has no filter
strip to display one, so a filter that survived the switch would be
invisible and the list would look like it had lost bookmarks.

The layout choice persists in @AppStorage. The toolbar button shows the
layout it switches *to* — a two-state toggle labelled with its current
state reads as a status light rather than a control.

Verified on an iPhone 17 Pro simulator against the live linkding server in
both layouts and both color schemes. Full test suite passes (16 tests).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
admin added 1 commit 2026-07-26 06:49:19 +00:00
Fix tap-to-open on cards, and move the list to the library design
CI / build-and-deploy (push) Successful in 21s
CI / build-and-deploy (pull_request) Successful in 20s
3c7a783b6c
Two changes.

Tapping a card did nothing. LibraryCard carried its own press-scale
effect via onLongPressGesture(minimumDuration: 0), and a zero-duration
long press fires immediately and swallows the tap the host attaches. It
was invisible in the prototype because nothing was listening for taps
there. LibraryCard is now purely presentational and the grid wraps it in
a Button with the existing RowPressStyle, which gets the same scale
without competing for the gesture.

The list now speaks the same design as the cards: paper ground, the
color chip where the favicon was, serif quoted title, monospaced
domain/date, serif-italic AI summary, monospaced tag chips. Everything
the old row carried is still there — unread state, excerpt, tags,
podcast affordance, reading progress — along with all swipe actions,
since it is still a List.

BookmarkListRow takes a style rather than being rewritten, because the
Tags and Search screens use the same row and should not be silently
restyled by a change aimed at the bookmarks screen.

The tag filter strip moved up to BookmarksView. Both layouts speak the
same language now, so the strip belongs to the screen rather than to one
mode — which also retires the rule that leaving cards mode had to clear
the tag filter to stop it becoming invisible.

The tag chip strip fades at its trailing edge; without it a tag clipped
mid-word reads as broken text rather than as something scrollable.

Verified in both layouts and both color schemes against the live server.
Full suite passes (16 tests).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
admin added 1 commit 2026-07-26 06:54:54 +00:00
Scale library type up 50%
CI / build-and-deploy (push) Successful in 21s
CI / build-and-deploy (pull_request) Successful in 19s
cdbec4aaf8
Every font size in the library design multiplied by 1.5 — card title and
stamp, list title, meta, excerpt and tags, filter tabs, and the
prototype's header.

Geometry had to follow, or the larger type would have broken the layout
it sits in:

- The card grid drops from three columns to two. At 16.5pt in a 95pt
  column a card gets about nine characters per line and every title
  truncates; the text size is what sets the column count.
- Filter tabs grow 30pt -> 42pt tall, the card gains padding and a
  slightly squarer aspect, and the list's color chip, unread dot and tag
  chips scale with the text they sit beside.

Verified in both layouts against the live server. Suite passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
admin added 1 commit 2026-07-26 06:59:21 +00:00
Wrap list tags onto two lines instead of scrolling
CI / build-and-deploy (pull_request) Successful in 20s
CI / build-and-deploy (push) Successful in 20s
0b8b9fe512
At the scaled-up type a horizontal tag strip clipped its third chip
mid-word, and a fade only made the clipping prettier. Tags now wrap: you
see whole tags or none.

SwiftUI has no wrapping stack, so this adds a small FlowLayout capped at
maxRows. Subviews past the cap are placed off-screen at zero size rather
than left unplaced — a Layout that declines to place a subview gets it
laid out at the origin instead of dropped, which would have stacked the
leftover tags on top of the first row. Verified by temporarily forcing
maxRows to 1: the overflow disappears cleanly, with no ghost chips.

Worth knowing: the cap does discard tags on real data. The heaviest
bookmarks carry five linkding tags plus AI tags, and two rows hold about
four chips at this size, so the tail is hidden rather than truncated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
admin added 1 commit 2026-07-26 07:02:22 +00:00
Label the tag-pin button for VoiceOver
CI / build-and-deploy (pull_request) Successful in 20s
CI / build-and-deploy (push) Successful in 20s
69c2bc7514
The + in the filter strip was an unlabeled SF Symbol, so VoiceOver
announced nothing useful for it. Also makes the control queryable, which
is how the menu was verified.

Verified by driving the real UI with a temporary XCUITest target: tapping
+ opens a menu listing the available tags, and tapping a card opens the
browser. The target was removed afterwards rather than committed — both
tests need the live linkding server to have loaded bookmarks first, which
is not something the unit suite should depend on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
admin added 1 commit 2026-07-26 16:12:49 +00:00
Replace the Tags tab with a tag picker sheet
CI / build-and-deploy (push) Successful in 32s
CI / build-and-deploy (pull_request) Successful in 31s
8a777ffc83
The tab bar drops from five to four; Tags now opens as a sheet from the +
in the library's filter strip, which is the control that was already
about adding a tag.

Picking a tag pins it as a filter tab rather than pushing to a separate
per-tag list. That made TagBookmarksView redundant — the filter tab shows
the same thing, in whichever layout you're already using — so it's gone.

This also fixes what the + could reach. The old inline menu listed tags
found on the loaded page, so it offered 21 of 96 tags, and once a tag
filter was active it collapsed to just the tags co-occurring with that
one — pinning a second unrelated tag was impossible. The sheet sources
names from the tags endpoint via a new BookmarksViewModel.loadAllTags(),
and counts still come from loaded bookmarks, so a tag we haven't paged in
shows no number rather than a wrong one.

The sheet is searchable, marks already-pinned tags with a check, and
picking a pinned tag selects that tab instead of duplicating it.

Verified by driving the UI: the tab bar is now Bookmarks/Sources/
Podcasts/Search, + opens the sheet with 42 rows where the old menu had
21, and picking "ai" pinned an "ai" tab. Suite passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
admin changed title from Add Library browse-screen prototype (paper/serif, color blocks, light + dark) to Library design: cards + redesigned list on the bookmarks screen, tag picker sheet 2026-07-26 16:55:27 +00:00
admin added 1 commit 2026-07-27 17:50:50 +00:00
Carry the library design across the rest of the app
CI / build-and-deploy (push) Successful in 34s
CI / build-and-deploy (pull_request) Successful in 34s
fffb3999bf
Every screen now speaks the paper vocabulary rather than only the
bookmarks screen: Search, Sources, Podcasts and the player, Settings,
Add/Edit, Smart Collections, Ask, Onboarding, the browser toolbar, the
Siri snippets, and the share extension's save card.

The change is mostly a design system rather than per-screen tweaks.
LibraryKit gains:

- Semantic colors — secondary/tertiary/faint text, `raised` surfaces,
  and accent/alarm/affirm, so screens stop reaching for .secondary,
  systemGray, .blue, .red and .green. The three status colors come out
  of the palette (the swatch blue, vermilion and forest) rather than
  from the system set, so the design keeps spending one set of inks.
- PaperType — the two voices made explicit. Prose (titles, summaries,
  anything a person wrote) is serif; anything the machine contributes
  (domains, dates, counts, tags, labels, buttons) is monospaced. Keeping
  that split strict is what makes the design read as archival rather
  than as decoration.
- paperSurface() / paperField() / paperCard() for grounds, inputs and
  raised blocks.
- PaperEmptyState, because ContentUnavailableView can't be restyled — it
  draws its own bold system type and grey, which was the loudest
  remaining system voice once the screens moved onto the sheet. All nine
  usages are converted.

The app also sets one accent for the system chrome it doesn't draw —
tab bar, search fields, switches, swipe actions — which otherwise stayed
system blue around paper screens.

BookmarkRow is deleted. Once Search moved to the library row and
TagBookmarksView was gone, nothing passed .classic, so the style fork in
BookmarkListRow went with it. There is one bookmark row now.

The share extension compiles LibraryKit directly, since its save card is
a user-facing surface and should not be the one place still using system
styling.

Verified on device in both color schemes; screens toured with a
temporary UI test harness (removed). Suite passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
admin added 1 commit 2026-07-27 18:17:06 +00:00
Set the navigation and tab bars in the library's type
CI / build-and-deploy (push) Successful in 28s
CI / build-and-deploy (pull_request) Successful in 27s
dd1c09a00c
The screen titles were still system bold sans — `navigationTitle` renders
through UIKit, which no SwiftUI font modifier reaches, so paperSurface()
could tint the bar but never restyle its text. PaperAppearance configures
UINavigationBarAppearance once at launch: serif large and inline titles in
ink, and monospaced tab bar labels.

One trap worth recording: configuring that appearance with
configureWithOpaqueBackground() makes iOS 26 stop laying out the large
title entirely — it vanishes rather than restyling, and it does so even
with the font attributes removed, so it reads like a font problem when it
isn't. Transparent works, and is right here anyway: every screen already
paints the paper ground itself.

Verified in both schemes. Suite passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
admin added 1 commit 2026-07-27 19:04:24 +00:00
Stop paperSurface() undoing the serif navigation titles
CI / build-and-deploy (push) Successful in 32s
CI / build-and-deploy (pull_request) Failing after 2s
76df237473
Only the bookmarks screen had picked up the serif title. Every other
screen — Search, Sources, Podcasts, Settings, Ask, the sheets — was still
system bold sans, and the reason was paperSurface(): setting
.toolbarBackground makes SwiftUI build a fresh UINavigationBarAppearance
and discard the one PaperAppearance installed, text attributes included.
Bookmarks was the only screen not using the helper, which is why it alone
looked right.

The modifier no longer sets a toolbar background. It doesn't need one —
the bar is transparent by appearance and the screen already paints the
paper ground beneath it.

Also labels the bookmarks toolbar buttons (Settings, Add bookmark, Unread
filter, AI actions), which were bare SF Symbols announcing nothing to
VoiceOver.

Verified in both schemes: large titles on Bookmarks/Sources/Podcasts/
Search and inline titles on the Settings and Ask sheets all render serif,
on paper grounds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
admin added 1 commit 2026-07-28 00:12:28 +00:00
Fix Spotlight indexing: route the entity URL to contentURL
CI / build-and-deploy (pull_request) Successful in 25s
CI / build-and-deploy (push) Successful in 26s
69556afc08
Every bookmark failed to reach the Spotlight index. Each item died in
translation with "Provided object for field url is of class NSURL,
expected class: NSString", so on-device search and Ask Your Bookmarks
were retrieving from an index that was effectively empty.

Left to itself, App Intents indexes BookmarkEntity's `url` property under
the attribute set's own `url` key, which Spotlight's Cascade translator
types as NSString. Giving the property an explicit
`indexingKey: \.contentURL` sends it to a URL-typed field instead — and
contentURL is the right field for "where this content lives" regardless.
Keeping the property a URL rather than retyping it to String means
existing Shortcuts that read it are unaffected.

The attribute set now sets contentURL too, and the retrieval side reads
it back, so the round trip stays on one field.

Why it went unnoticed: translation happens after `indexAppEntities`
returns, so indexing logged success the whole time. Measured on the
simulator against the live library — 202 translation failures per launch
before, 0 after.

Adds SpotlightRetrievalTests, which indexes a bookmark and retrieves it
through the assistant's own path. Nothing weaker would have caught this,
since the failure was silent at every layer above the index.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
admin merged commit 1995b38b52 into master 2026-07-30 04:03:03 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/linkding-ios#9