Linked References — Shaping
Frame
Problem
- The guide page links to essays that expand on concepts, but clicking navigates away — losing the reader's place and breaking flow
- Essays link to other essays and /note/ pages inline, but there's no way to peek at referenced content without committing to a full navigation
- The site has a rich internal link network (guide → essays, essays → essays, essays → notes) but no way to preview or explore connections without leaving the current page
Outcome
- Readers can preview linked content without leaving their current page
- Readers can open linked content alongside the current page for deeper reference
- The interaction feels natural and lightweight — enhancing reading flow rather than interrupting it
Requirements (R)
| ID | Requirement | Status |
|---|---|---|
| R0 | Preview linked internal content without navigating away | Core goal |
| R1 | Open linked content in a closeable side panel | Must-have |
| R2 | Works for all internal link types (guide→essay, essay→essay, essay→note) | Must-have |
| R3 | Works on mobile (side panel becomes overlay or bottom sheet) | Must-have |
| R4 | Regular links navigate normally (shift+click opens panel); reference-configured links open panel by default (double-click navigates) | Must-have |
| R5 | Content fetched on panel open; preloading out of scope for now | Nice-to-have |
| R6 | Static site compatible (no server-side runtime) | Must-have |
| R7 | Links inside the panel replace the panel content | Must-have (revisit later) |
| R8 | Hovering an internal link shows a tooltip preview of the linked content | Must-have |
Open requirements (not yet decided)
- How reference links are authored (MDX component? data attribute? other syntax?)
- Whether panel open state affects the URL
Shapes
Shape A: Vanilla JS + event delegation
| Part | Mechanism |
|---|---|
| A1 | JS module on every page intercepts internal link hover/click via event delegation on document |
| A2 | Tooltip: fetch linked page HTML, extract <main>, show floating preview near cursor |
| A3 | Panel: fetch linked page HTML, extract <main>, inject into fixed side panel DOM element |
| A4 | Reference links: MDX component <Ref href="..."> renders <a data-ref href="..."> |
| A5 | Mobile: panel becomes full-width overlay instead of side-by-side |
Tradeoffs:
- Simpler, no framework overhead
- Harder to manage complex state (loading, history within panel) as feature grows
- Works with Astro's static output with no hydration cost
Shape B: Preact Island
| Part | Mechanism |
|---|---|
| B1 | <LinkManager> Preact island wraps page content, manages panel/tooltip state reactively |
| B2 | Same fetch + HTML extraction as A2/A3 |
| B3 | Same <Ref> MDX component as A4 |
| B4 | Mobile: same overlay behavior, managed via reactive state |
Tradeoffs:
- More complex setup, adds Preact hydration to pages
- Easier to extend as feature grows (loading states, back/forward within panel, etc.)
- Reactive state is a natural fit for open/closed, loading, current content
Selected: Shape A (refined)
Research confirmed Astro can generate a static JSON manifest at build time via an endpoint. This makes tooltips instant (no per-link fetch). Only the panel fetch needs to hit full HTML.
| Part | Mechanism |
|---|---|
| A1 | Build-time Astro endpoint generates /api/pages.json with title, excerpt, slug for every page |
| A2 | Vanilla JS module fetches manifest once on page load, caches in memory |
| A3 | Event delegation on document intercepts hover and click on all internal links |
| A4 | Tooltip: look up page in manifest (instant), position with Floating UI, show on hover |
| A5 | Panel: fetch full page HTML on open, extract <main>, inject into fixed side panel DOM element |
| A6 | Reference links: MDX component <Ref href="..."> renders <a data-ref href="..."> |
| A7 | Mobile: panel becomes full-width overlay instead of side-by-side |
Fit Check (R × A)
| Req | Requirement | Status | A |
|---|---|---|---|
| R0 | Preview linked internal content without navigating away | Core goal | ✅ |
| R1 | Open linked content in a closeable side panel | Must-have | ✅ |
| R2 | Works for all internal link types (guide→essay, essay→essay, essay→note) | Must-have | ✅ |
| R3 | Works on mobile (side panel becomes overlay or bottom sheet) | Must-have | ✅ |
| R4 | Regular links navigate normally (shift+click opens panel); reference-configured links open panel by default (double-click navigates) | Must-have | ✅ |
| R5 | Content fetched on panel open; preloading out of scope for now | Nice-to-have | ✅ |
| R6 | Static site compatible (no server-side runtime) | Must-have | ✅ |
| R7 | Links inside the panel replace the panel content | Must-have (revisit later) | ✅ |
| R8 | Hovering an internal link shows a tooltip preview of the linked content | Must-have | ✅ |
Breadboard
Places
| # | Place | Description |
|---|---|---|
| P_B | Build | Astro build step |
| P1 | Content Page | Any page with internal links (guide, essay, note) |
| P2 | Side Panel | Reference content viewer; full overlay on mobile |
UI Affordances
| # | Place | Component | Affordance | Control | Wires Out | Returns To |
|---|---|---|---|---|---|---|
| U1 | P1 | content | regular internal link | hover / shift+click / click | → N3, → N7 | — |
| U2 | P1 | content | reference link (data-ref) | hover / click / double-click | → N3, → N7 | — |
| U3 | P1 | tooltip | tooltip preview | render | — | — |
| U4 | P2 | side-panel | panel content area | render | — | — |
| U5 | P2 | side-panel | close button | click | → N11 | — |
| U6 | P2 | side-panel | panel link | click | → N13 | — |
Code Affordances
| # | Place | Component | Affordance | Control | Wires Out | Returns To |
|---|---|---|---|---|---|---|
| N_B1 | P_B | astro | pages.json.ts endpoint | build | → S1 | — |
| N1 | P1 | link-manager | initLinkManager() | page load | → N2, → N3, → N5, → N7 | — |
| N2 | P1 | link-manager | fetch('/api/pages.json') | call | — | → S2 |
| N3 | P1 | link-manager | document mouseenter listener | observe | → N4 | — |
| N4 | P1 | link-manager | showTooltip(url, el) | call | → U3 | — |
| N5 | P1 | link-manager | document mouseleave listener | observe | → N6 | — |
| N6 | P1 | link-manager | hideTooltip() | call | → U3 | — |
| N7 | P1 | link-manager | document click listener | observe | → N8 | — |
| N8 | P1 | link-manager | handleLinkClick(event, el) | call | → N9 if shift+click or data-ref | — |
| N9 | P1 | link-manager | openPanel(url) | call | → N10 | — |
| N10 | P1 | link-manager | fetchPageContent(url) | call | — | → N12 |
| N11 | P2 | side-panel | closePanel() | call | → S3 | — |
| N12 | P2 | side-panel | renderPanel(content) | call | → S3, → U4 | — |
| N13 | P2 | side-panel | handlePanelLink(url) | call | → N10 | — |
Data Stores
| # | Place | Store | Description |
|---|---|---|---|
| S1 | P_B | /api/pages.json | Static manifest: [{url, title, excerpt}] |
| S2 | P1 | manifest cache | Map<url, {title, excerpt}> — in-memory, fetched once |
| S3 | P2 | panel state | {open: boolean, url: string, content: Element} |
Diagram
Open questions
- U3 tooltip: should
hideTooltip()(N6) toggle a CSS class rather than wire to U3 directly? - P2 as separate Place: on desktop the panel is non-blocking; on mobile it's a full overlay. Modeled as P2 in both cases for simplicity.
- Authoring syntax for reference links (
<Ref>component vs other) - Whether panel open state affects the URL